Browse Source

Initialize project structure with configuration files, add TRPC support, and implement window management. Introduce new packages for config, logger, and TRPC, along with necessary updates to existing files for improved functionality and organization.

main
dash 1 month ago
parent
commit
32fcfa3e86
  1. 0
      .env
  2. 1
      .npmrc
  3. 2
      .vscode/settings.json
  4. 16
      electron.vite.config.ts
  5. 8
      package.json
  6. 11
      packages/config/app_config.json
  7. 16
      packages/config/exe_config.json
  8. 18
      packages/config/index.ts
  9. 12
      packages/config/package.json
  10. 0
      packages/logger/index.ts
  11. 12
      packages/logger/package.json
  12. 8
      packages/trpc/common/app.ts
  13. 3
      packages/trpc/common/index.ts
  14. 19
      packages/trpc/common/routers/post.ts
  15. 38
      packages/trpc/common/routers/user.ts
  16. 8
      packages/trpc/common/trpc.ts
  17. 11
      packages/trpc/main/index.ts
  18. 12
      packages/trpc/package.json
  19. 5
      packages/trpc/preload/index.ts
  20. 4
      packages/trpc/renderer/index.ts
  21. 69
      pnpm-lock.yaml
  22. 2
      pnpm-workspace.yaml
  23. 34
      src/main/api.ts
  24. 9
      src/main/env.d.ts
  25. 106
      src/main/index.ts
  26. 359
      src/main/modules/window-manager/index.ts
  27. 107
      src/main/modules/window-manager/windowsMap.ts
  28. 30
      src/main/utils/base/base-singleton.ts
  29. 18
      src/main/utils/file.ts
  30. 8
      src/main/utils/url.ts
  31. 27
      src/preload/index.ts
  32. 32
      src/renderer/about.html
  33. 13
      src/renderer/src/App.vue
  34. 8
      src/renderer/src/about.ts
  35. 22
      tsconfig.node.json
  36. 3
      tsconfig.web.json

0
.env

1
.npmrc

@ -1,2 +1,3 @@
electron_mirror=https://npmmirror.com/mirrors/electron/
electron_builder_binaries_mirror=https://npmmirror.com/mirrors/electron-builder-binaries/
link-workspace-packages=true

2
.vscode/settings.json

@ -1,6 +1,6 @@
{
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
"editor.defaultFormatter": "vscode.typescript-language-features"
},
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"

16
electron.vite.config.ts

@ -4,6 +4,12 @@ import vue from '@vitejs/plugin-vue'
export default defineConfig({
main: {
resolve: {
alias: {
main: resolve('src/main'),
"@res": resolve("resources"),
}
},
plugins: [externalizeDepsPlugin()]
},
preload: {
@ -15,6 +21,14 @@ export default defineConfig({
'@renderer': resolve('src/renderer/src')
}
},
plugins: [vue()]
plugins: [vue()],
build: {
rollupOptions: {
input: {
main: resolve(__dirname, "./src/renderer/index.html"),
about: resolve(__dirname, "./src/renderer/about.html"),
},
},
},
}
})

8
package.json

@ -25,22 +25,28 @@
"@electron-toolkit/utils": "^4.0.0",
"@trpc/client": "^10.45.2",
"@trpc/server": "^10.45.2",
"electron-trpc": "^0.7.1",
"electron-updater": "^6.3.9",
"superjson": "^2.2.5",
"zod": "^4.1.12"
},
"devDependencies": {
"@electron-toolkit/eslint-config-prettier": "3.0.0",
"@electron-toolkit/eslint-config-ts": "^3.1.0",
"@electron-toolkit/tsconfig": "^2.0.0",
"@types/lodash-es": "^4.17.12",
"@types/node": "^22.18.6",
"@vitejs/plugin-vue": "^6.0.1",
"config": "workspace:^",
"electron": "^38.1.2",
"electron-builder": "^25.1.8",
"electron-trpc": "^0.7.1",
"electron-vite": "^4.0.1",
"eslint": "^9.36.0",
"eslint-plugin-vue": "^10.4.0",
"lodash-es": "^4.17.21",
"logger": "workspace:^",
"prettier": "^3.6.2",
"trpc": "workspace:^",
"typescript": "^5.9.2",
"vite": "^7.1.6",
"vue": "^3.5.21",

11
packages/config/app_config.json

@ -0,0 +1,11 @@
{
"storagePath": "$storagePath$",
"language": "zh",
"dev:debug": 2,
"common.theme": "auto",
"update.hoturl": "https://alist.xieyaxin.top/d/%E8%B5%84%E6%BA%90/%E6%B5%8B%E8%AF%95%E6%96%87%E4%BB%B6.zip?sign=eqy35CR-J1SOQZz0iUN2P3B0BiyZPdYH0362nLXbUhE=:1749085071",
"update.repo": "wood-desktop",
"update.owner": "npmrun",
"update.allowDowngrade": false,
"update.allowPrerelease": false
}

16
packages/config/exe_config.json

@ -0,0 +1,16 @@
{
"name": "zephyr",
"appId": "com.zephyr.app",
"win": {
"executableName": "zephyr"
},
"linux": {
"target": ["AppImage", "snap", "deb"],
"maintainer": "electronjs.org",
"category": "Utility"
},
"publish": {
"provider": "generic",
"url": "https://example.com/auto-updates"
}
}

18
packages/config/index.ts

@ -0,0 +1,18 @@
import AppConfig from "./app_config.json"
import ExeConfig from "./exe_config.json"
// 定义主题类型
type ThemeType = "light" | "dark" | "auto"
// 定义语言类型
type LanguageType = "zh" | "en"
export type IConfig = typeof AppConfig &
Pick<Partial<typeof AppConfig>, "common.theme"> & {
language: LanguageType
"common.theme": ThemeType
}
export default {
appConfig: AppConfig,
exeConfig: ExeConfig,
}

12
packages/config/package.json

@ -0,0 +1,12 @@
{
"name": "config",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}

0
packages/logger/index.ts

12
packages/logger/package.json

@ -0,0 +1,12 @@
{
"name": "logger",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}

8
packages/trpc/common/app.ts

@ -0,0 +1,8 @@
import { mergeRouters } from './trpc';
import { userRouter } from './routers/user';
import { postRouter } from './routers/post';
export const appRouter = mergeRouters(userRouter, postRouter)
export type AppRouter = typeof appRouter;

3
packages/trpc/common/index.ts

@ -0,0 +1,3 @@
import type { AppRouter } from './app';
export { AppRouter };

19
packages/trpc/common/routers/post.ts

@ -0,0 +1,19 @@
import { router, publicProcedure } from '../trpc';
import { z } from 'zod';
export const postRouter = router({
postCreate: publicProcedure
.input(
z.object({
title: z.string(),
}),
)
.mutation(() => {
// const { input } = opts;
// [...]
}),
postList: publicProcedure.query(() => {
// ...
return [];
}),
});

38
packages/trpc/common/routers/user.ts

@ -0,0 +1,38 @@
import { router, publicProcedure } from '../trpc';
import { observable } from '@trpc/server/observable';
import { EventEmitter } from 'events';
import { WindowManager } from 'main/modules/window-manager';
const ee = new EventEmitter();
export const userRouter = router({
userList: publicProcedure.query(() => {
// [..]
ee.emit('greeting', `Greeted`)
return [
{
id: 1,
name: 'John Doe',
windowsLength: WindowManager.getWindowsLength(),
},
{
id: 2,
name: 'Jane Doe',
windowsLength: WindowManager.getWindowsLength(),
},
];
}),
subscribeGreeting: publicProcedure.subscription(() => {
return observable<{ text: string }>((emit) => {
function onGreet(text: string) {
emit.next({ text });
}
ee.on('greeting', onGreet);
return () => {
ee.off('greeting', onGreet);
};
});
}),
});

8
packages/trpc/common/trpc.ts

@ -0,0 +1,8 @@
import { initTRPC } from '@trpc/server';
import superjson from 'superjson';
const t = initTRPC.create({ transformer: superjson, isServer: true });
export const mergeRouters = t.mergeRouters;
export const router = t.router;
export const publicProcedure = t.procedure;

11
packages/trpc/main/index.ts

@ -0,0 +1,11 @@
import { appRouter } from "../common/app";
import { BrowserWindow } from "electron";
import { createIPCHandler } from 'electron-trpc/main';
const handler = createIPCHandler({ router: appRouter, windows: []});
export function trpcBindWindows(windows: BrowserWindow[]) {
windows.forEach(window => {
handler.attachWindow(window);
});
}

12
packages/trpc/package.json

@ -0,0 +1,12 @@
{
"name": "trpc",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}

5
packages/trpc/preload/index.ts

@ -0,0 +1,5 @@
import { exposeElectronTRPC } from 'electron-trpc/main';
process.once('loaded', async () => {
exposeElectronTRPC();
});

4
src/renderer/src/trpc.ts → packages/trpc/renderer/index.ts

@ -1,7 +1,9 @@
import { createTRPCProxyClient } from '@trpc/client';
import { ipcLink } from 'electron-trpc/renderer';
import type { AppRouter } from '../../main/api';
import superjson from 'superjson';
import type { AppRouter } from '../common';
export const client = createTRPCProxyClient<AppRouter>({
links: [ipcLink()],
transformer: superjson,
});

69
pnpm-lock.yaml

@ -20,12 +20,12 @@ importers:
'@trpc/server':
specifier: ^10.45.2
version: 10.45.2
electron-trpc:
specifier: ^0.7.1
version: 0.7.1(@trpc/client@10.45.2(@trpc/server@10.45.2))(@trpc/server@10.45.2)(electron@38.4.0)
electron-updater:
specifier: ^6.3.9
version: 6.6.2
superjson:
specifier: ^2.2.5
version: 2.2.5
zod:
specifier: ^4.1.12
version: 4.1.12
@ -39,18 +39,27 @@ importers:
'@electron-toolkit/tsconfig':
specifier: ^2.0.0
version: 2.0.0(@types/node@22.18.12)
'@types/lodash-es':
specifier: ^4.17.12
version: 4.17.12
'@types/node':
specifier: ^22.18.6
version: 22.18.12
'@vitejs/plugin-vue':
specifier: ^6.0.1
version: 6.0.1(vite@7.1.12(@types/node@22.18.12))(vue@3.5.22(typescript@5.9.3))
config:
specifier: workspace:^
version: link:packages/config
electron:
specifier: ^38.1.2
version: 38.4.0
electron-builder:
specifier: ^25.1.8
version: 25.1.8(electron-builder-squirrel-windows@25.1.8)
electron-trpc:
specifier: ^0.7.1
version: 0.7.1(@trpc/client@10.45.2(@trpc/server@10.45.2))(@trpc/server@10.45.2)(electron@38.4.0)
electron-vite:
specifier: ^4.0.1
version: 4.0.1(vite@7.1.12(@types/node@22.18.12))
@ -60,9 +69,18 @@ importers:
eslint-plugin-vue:
specifier: ^10.4.0
version: 10.5.1(@typescript-eslint/parser@8.46.2(eslint@9.38.0)(typescript@5.9.3))(eslint@9.38.0)(vue-eslint-parser@10.2.0(eslint@9.38.0))
lodash-es:
specifier: ^4.17.21
version: 4.17.21
logger:
specifier: workspace:^
version: link:packages/logger
prettier:
specifier: ^3.6.2
version: 3.6.2
trpc:
specifier: workspace:^
version: link:packages/trpc
typescript:
specifier: ^5.9.2
version: 5.9.3
@ -79,6 +97,12 @@ importers:
specifier: ^3.0.7
version: 3.1.2(typescript@5.9.3)
packages/config: {}
packages/logger: {}
packages/trpc: {}
packages:
7zip-bin@5.2.0:
@ -654,6 +678,12 @@ packages:
'@types/keyv@3.1.4':
resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==}
'@types/lodash-es@4.17.12':
resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==}
'@types/lodash@4.17.20':
resolution: {integrity: sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==}
'@types/ms@2.1.0':
resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
@ -1068,6 +1098,10 @@ packages:
convert-source-map@2.0.0:
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
copy-anything@4.0.5:
resolution: {integrity: sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==}
engines: {node: '>=18'}
core-util-is@1.0.2:
resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==}
@ -1661,6 +1695,10 @@ packages:
resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==}
engines: {node: '>=10'}
is-what@5.5.0:
resolution: {integrity: sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==}
engines: {node: '>=18'}
isarray@1.0.0:
resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
@ -1736,6 +1774,9 @@ packages:
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
engines: {node: '>=10'}
lodash-es@4.17.21:
resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==}
lodash.defaults@4.2.0:
resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==}
@ -2269,6 +2310,10 @@ packages:
resolution: {integrity: sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==}
engines: {node: '>= 8.0'}
superjson@2.2.5:
resolution: {integrity: sha512-zWPTX96LVsA/eVYnqOM2+ofcdPqdS1dAF1LN4TS2/MWuUpfitd9ctTa87wt4xrYnZnkLtS69xpBdSxVBP5Rm6w==}
engines: {node: '>=16'}
supports-color@7.2.0:
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
engines: {node: '>=8'}
@ -3039,6 +3084,12 @@ snapshots:
dependencies:
'@types/node': 22.18.12
'@types/lodash-es@4.17.12':
dependencies:
'@types/lodash': 4.17.20
'@types/lodash@4.17.20': {}
'@types/ms@2.1.0': {}
'@types/node@22.18.12':
@ -3596,6 +3647,10 @@ snapshots:
convert-source-map@2.0.0: {}
copy-anything@4.0.5:
dependencies:
is-what: 5.5.0
core-util-is@1.0.2:
optional: true
@ -4308,6 +4363,8 @@ snapshots:
is-unicode-supported@0.1.0: {}
is-what@5.5.0: {}
isarray@1.0.0: {}
isbinaryfile@4.0.10: {}
@ -4376,6 +4433,8 @@ snapshots:
dependencies:
p-locate: 5.0.0
lodash-es@4.17.21: {}
lodash.defaults@4.2.0: {}
lodash.difference@4.5.0: {}
@ -4916,6 +4975,10 @@ snapshots:
transitivePeerDependencies:
- supports-color
superjson@2.2.5:
dependencies:
copy-anything: 4.0.5
supports-color@7.2.0:
dependencies:
has-flag: 4.0.0

2
pnpm-workspace.yaml

@ -0,0 +1,2 @@
packages:
- "packages/*"

34
src/main/api.ts

@ -1,34 +0,0 @@
import z from 'zod';
import { initTRPC } from '@trpc/server';
import { observable } from '@trpc/server/observable';
import { EventEmitter } from 'events';
const ee = new EventEmitter();
const t = initTRPC.create({ isServer: true });
export const router = t.router({
greeting: t.procedure.input(z.object({ name: z.string() })).query((req) => {
const { input } = req;
ee.emit('greeting', `Greeted ${input.name}`);
return {
text: `Hello ${input.name}` as const,
};
}),
subscript: t.procedure.subscription(() => {
return observable((emit) => {
function onGreet(text: string) {
emit.next({ text });
}
ee.on('greeting', onGreet);
return () => {
ee.off('greeting', onGreet);
};
});
}),
});
export type AppRouter = typeof router;

9
src/main/env.d.ts

@ -0,0 +1,9 @@
/// <reference types="vite/client" />
interface ImportMetaEnv {
// readonly MAIN_VITE_DEBUG: string
}
interface ImportMeta {
readonly env: ImportMetaEnv
}

106
src/main/index.ts

@ -1,76 +1,44 @@
import { app, shell, BrowserWindow, ipcMain } from 'electron'
import { join } from 'path'
import { electronApp, optimizer, is } from '@electron-toolkit/utils'
import icon from '../../resources/icon.png?asset'
import { createIPCHandler } from 'electron-trpc/main';
import { router } from './api';
import { app, ipcMain, nativeTheme } from 'electron'
import config from 'config'
import { electronApp, } from '@electron-toolkit/utils'
import WindowManager from './modules/window-manager';
import icon from "@res/icon.png?asset"
import { getFileUrl, getPreloadUrl } from './utils/file';
function createWindow(): void {
// Create the browser window.
const mainWindow = new BrowserWindow({
width: 900,
height: 670,
show: false,
autoHideMenuBar: true,
...(process.platform === 'linux' ? { icon } : {}),
webPreferences: {
preload: join(__dirname, '../preload/index.js'),
sandbox: false
}
})
createIPCHandler({ router, windows: [mainWindow] });
mainWindow.on('ready-to-show', () => {
mainWindow.show()
})
app.commandLine.appendSwitch("wm-window-animations-disabled")
app.disableHardwareAcceleration()
mainWindow.webContents.setWindowOpenHandler((details) => {
shell.openExternal(details.url)
return { action: 'deny' }
})
// HMR for renderer base on electron-vite cli.
// Load the remote URL for development or the local html file for production.
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL'])
} else {
mainWindow.loadFile(join(__dirname, '../renderer/index.html'))
}
}
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.whenReady().then(() => {
// Set app user model id for windows
electronApp.setAppUserModelId('com.electron')
// Default open or close DevTools by F12 in development
// and ignore CommandOrControl + R in production.
// see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils
app.on('browser-window-created', (_, window) => {
optimizer.watchWindowShortcuts(window)
electronApp.setAppUserModelId(config.exeConfig.appId)
nativeTheme.themeSource = "system"
WindowManager.showMainWindow()
ipcMain.on('ping', () => {
WindowManager.createWindow("about", {
url: getFileUrl("about.html"),
overideWindowOpts: true,
confrimWindowClose: false,
type: "info",
windowOpts: {
width: 600,
height: 400,
minimizable: false,
darkTheme: true,
modal: true,
title: "关于我",
show: true,
resizable: false,
icon: icon,
webPreferences: {
preload: getPreloadUrl("index"),
devTools: false,
sandbox: false,
nodeIntegration: false,
contextIsolation: true,
},
},
})
// IPC test
ipcMain.on('ping', () => console.log('pong'))
createWindow()
app.on('activate', function () {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})
// Quit when all windows are closed, except on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q.
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
})
// In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and require them here.

359
src/main/modules/window-manager/index.ts

@ -0,0 +1,359 @@
import { BrowserWindow, app, dialog } from "electron"
import { cloneDeep, merge } from "lodash-es"
import { defaultConfig, defaultWindowConfig, getWindowsMap, IConfig, Param } from "./windowsMap"
import { optimizer } from "@electron-toolkit/utils"
import { BaseSingleton } from "../../utils/base/base-singleton"
import { trpcBindWindows } from 'trpc/main';
declare module "electron" {
interface BrowserWindow {
$$forceClose?: boolean
$$lastChoice?: number
$$opts?: Param
}
}
class WindowManagerClass extends BaseSingleton {
init(): void {
this.isMainShowReady = new Promise(resolve => {
this.isMainShowResolve = resolve
})
/**
*
*/
app.on("activate", () => {
this.showMainWindow()
})
// Default open or close DevTools by F12 in development
// and ignore CommandOrControl + R in production.
// see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils
app.on("browser-window-created", (_, window) => {
optimizer.watchWindowShortcuts(window)
})
/**
* event.preventDefault()
* 1. autoUpdater.quitAndInstall()close事件之后执行
* 2. 退
*/
app.on("before-quit", (event: Electron.Event) => {
const mainWin = this.get(this.mainInfo.name)
if (!mainWin || (mainWin && mainWin?.$$forceClose)) {
// app.exit()
} else {
event.preventDefault()
}
})
app.on("window-all-closed", () => {
if (process.platform !== "darwin") {
app.quit()
}
})
}
globalChioce: number = -1
#showWin(info: Param) {
if (this.#windows.length >= 6) {
dialog.showErrorBox("错误", "窗口数量超出限制")
return
}
if (!info.name) {
dialog.showErrorBox("错误", "窗口未指定唯一key")
return
}
const index = this.findIndex(info.name)
if (index === -1) {
this.#windows.push(this.#add(info))
} else {
if (this.#windows[index].isDestroyed()) {
this.#windows[index] = this.#add(info)
} else {
if (info.url && info.loadURLInSameWin) {
this.#windows[index].loadURL(info.url)
}
this.#windows[index].show()
}
}
}
showMainWindow() {
this.#showWin(this.mainInfo)
this.isMainShowResolve()
}
private isMainShowResolve
private isMainShowReady
async waitMainShowReady() {
await this.isMainShowReady
}
createWindow(name: string, opts?: Partial<IConfig>) {
const info = opts as Param
info.name = name
if (!info.ignoreEmptyUrl && !info.url) {
dialog.showErrorBox("错误", name + "窗口未提供url")
return
}
this.#showWin(info as Param)
}
showWindow(name: string, opts?: Partial<IConfig>) {
let have = false
for (const key in this.#urlMap) {
const info = this.#urlMap[key]
if (new RegExp(key).test(name)) {
opts && merge(info, opts)
info.name = name
if (!info.ignoreEmptyUrl && !info.url) {
dialog.showErrorBox("错误", name + "窗口未提供url")
return
}
this.#showWin(info as Param)
have = true
}
}
if (!have) {
dialog.showErrorBox("错误", name + "窗口未创建成功")
return
}
}
#urlMap = getWindowsMap()
getWndows() {
return this.#windows
}
length() {
return this.#windows.length
}
public get mainInfo() {
return this.#urlMap["main"] as Param
}
#windows: BrowserWindow[] = []
#defaultConfig: IConfig = defaultConfig
#add(config: Param) {
const curConfig = cloneDeep(this.#defaultConfig ?? {}) as Omit<IConfig, "name"> & { name: string }
for (const key in config) {
if (Object.prototype.hasOwnProperty.call(config, key)) {
const value = config[key]
// if (Reflect.has(curConfig, key)) {
curConfig[key] = value
// }
}
}
const privateConfig = merge(curConfig.overideWindowOpts ? {} : cloneDeep(defaultWindowConfig), curConfig.windowOpts ?? {})
let parentWindow
if (typeof privateConfig.parent === "string") {
parentWindow = this.get(privateConfig.parent)
}
if (parentWindow) {
privateConfig.parent = parentWindow
}
const browserWin = new BrowserWindow(privateConfig)
browserWin.webContents.setWindowOpenHandler(() => {
if (curConfig.denyWindowOpen) {
return { action: "deny" }
}
return { action: "allow" }
})
// @ts-ignore 不需要解释为啥
browserWin.webContents.$$senderName = curConfig.name
browserWin.$$forceClose = false
browserWin.$$lastChoice = -1
browserWin.on("close", (event: any) => {
if (this.globalChioce === 1) {
this.#onClose(curConfig.name)
return
}
if (!curConfig.confrimWindowClose) {
this.#onClose(curConfig.name)
return
}
// eslint-disable-next-line @typescript-eslint/no-this-alias
const that = this
function justQuit() {
browserWin.$$lastChoice = 1
// app.quit()
// 不要用quit();试了会弹两次
browserWin.$$forceClose = true
if (curConfig.name === that.mainInfo.name) {
that.globalChioce = 1
app.quit() // exit()直接关闭客户端,不会执行quit();
} else {
that.delete(curConfig.name)
}
}
if (browserWin.$$forceClose) {
that.delete(curConfig.name)
app.quit()
} else {
let choice = -1
if (browserWin && browserWin!.$$lastChoice !== undefined && browserWin.$$lastChoice >= 0) {
choice = browserWin.$$lastChoice
} else {
choice = dialog.showMessageBoxSync(browserWin, {
type: "info",
title: curConfig.confrimWindowCloseText?.title ?? "确认关闭",
defaultId: curConfig.confrimWindowCloseText?.defaultId ?? 0,
cancelId: curConfig.confrimWindowCloseText?.cancelId ?? 0,
message: curConfig.confrimWindowCloseText?.message ?? "",
buttons: curConfig.confrimWindowCloseText?.buttons ?? ["确定", "取消"],
})
}
if (choice === 1) {
justQuit()
} else {
event && event.preventDefault()
}
}
})
browserWin.$$opts = curConfig
// 在此注册窗口
browserWin.webContents.addListener("did-finish-load", () => {
browserWin.webContents.executeJavaScript(`window._global=${JSON.stringify({ name: curConfig.name })};`)
browserWin.webContents.send("bind-window-manager", curConfig.name)
})
// https://www.electronjs.org/zh/docs/latest/tutorial/security#12-%E5%88%9B%E5%BB%BAwebview%E5%89%8D%E7%A1%AE%E8%AE%A4%E5%85%B6%E9%80%89%E9%A1%B9
// browserWin.webContents.on("will-attach-webview", (_event, webPreferences) => {
// if (webPreferences.preload !== path.resolve(app.getAppPath(), "webview.js")) {
// // 如果未使用,则删除预加载脚本或验证其位置是否合法
// delete webPreferences.preload
// }
// // 禁用 Node.js 集成
// webPreferences.nodeIntegration = false
// // 验证正在加载的 URL
// // if (!params.src.startsWith('https://example.com/')) {
// // event.preventDefault()
// // }
// })
if (curConfig.type === "info") {
// 隐藏菜单
browserWin.setMenuBarVisibility(false)
}
if (curConfig.url) {
browserWin.loadURL(curConfig.url)
// logger.debug(`当前窗口网址:${curConfig.url}`)
}
if (curConfig.windowOpts?.show === false) {
if (curConfig.url) {
browserWin.once("ready-to-show", () => {
browserWin?.show()
})
} else {
browserWin?.show()
}
}
trpcBindWindows([browserWin])
return browserWin
}
getWindowsLength() {
return this.#windows.length
}
#onClose(name: string) {
for (let i = this.#windows.length - 1; i >= 0; i--) {
const win = this.#windows[i]
if (name === win.$$opts!.name) {
win.destroy()
this.#windows.splice(i, 1)
}
}
}
get(name: string) {
return this.#windows.find(v => {
return v.$$opts!.name === name
})
}
getFocusWindow() {
const mainWindow = this.getMainWindow()
if (mainWindow?.isFocused()) {
return mainWindow
}
for (let i = 0; i < this.#windows.length; i++) {
const win = this.#windows[i]
if (win.isFocused()) {
return win
}
}
return
}
getMainWindow() {
return this.#windows.find(v => {
return v.$$opts!.name === this.mainInfo.name
})
}
close(name: string | RegExp) {
const indexList = this.findAllIndex(name)
for (let i = indexList.length - 1; i >= 0; i--) {
const index = indexList[i]
const win = this.#windows[index]
win.close()
}
}
delete(name: string | RegExp) {
const indexList = this.findAllIndex(name)
for (let i = indexList.length - 1; i >= 0; i--) {
const index = indexList[i]
this.#windows.splice(index, 1)
}
}
findIndex(name: string | RegExp) {
const index = this.#windows.findIndex(v => {
if (typeof name === "string") {
return v.$$opts!.name === name
} else {
return name.test(v.$$opts!.name)
}
})
return index
}
findAllIndex(name: string | RegExp) {
const result: number[] = []
for (let i = 0; i < this.#windows.length; i++) {
const win = this.#windows[i]
if (typeof name === "string" && win.$$opts!.name === name) {
result.push(i)
} else if (typeof name !== "string" && name.test(win.$$opts!.name)) {
result.push(i)
}
}
return result
}
// show(name: string | RegExp) {
// let indexList = this.findAllIndex(name)
// if (!!indexList.length) {
// for (let i = 0; i < indexList.length; i++) {
// const index = indexList[i];
// const win = this.#windows[index]
// if (win.isDestroyed()) {
// this.#windows[index] = this.#add(win.$$opts)
// } else {
// win.show()
// }
// }
// } else {
// console.warn("该窗口不存在")
// }
// }
}
const WindowManager = WindowManagerClass.getInstance<WindowManagerClass>()
export { WindowManager }
export default WindowManager

107
src/main/modules/window-manager/windowsMap.ts

@ -0,0 +1,107 @@
import Config from "config"
import { BrowserWindowConstructorOptions } from "electron"
import icon from "@res/icon.png?asset"
import { getFileUrl, getPreloadUrl } from "../../utils/file"
export type Param = Partial<IConfig> & Required<Pick<IConfig, "name">>
export interface IConfig {
name?: string
url?: string
loadURLInSameWin?: boolean
type?: "info"
windowOpts?: BrowserWindowConstructorOptions
overideWindowOpts?: boolean
ignoreEmptyUrl?: boolean
denyWindowOpen?: boolean
confrimWindowClose?: boolean
confrimWindowCloseText?: {
title: string
message: string
buttons: string[]
defaultId: number
cancelId: number
}
}
export const defaultConfig: IConfig = {
denyWindowOpen: true,
}
export const defaultWindowConfig = {
height: 600,
useContentSize: true,
width: 800,
show: true,
resizable: true,
minWidth: 900,
minHeight: 600,
frame: true,
transparent: false,
alwaysOnTop: false,
webPreferences: {},
}
export function getWindowsMap(): Record<string, IConfig> {
return {
main: {
name: "main",
url: getFileUrl("index.html"),
confrimWindowClose: true,
confrimWindowCloseText: {
title: Config.exeConfig.name,
defaultId: 0,
cancelId: 0,
message: "确定要关闭吗?",
buttons: ["没事", "直接退出"],
},
windowOpts: {
show: false,
titleBarStyle: "hidden",
titleBarOverlay: true,
icon: icon,
...(process.platform === "linux" ? { icon } : {}),
webPreferences: {
webviewTag: false,
preload: getPreloadUrl("index"),
nodeIntegration: false,
contextIsolation: true,
},
},
},
_blank: {
overideWindowOpts: false,
confrimWindowClose: true,
confrimWindowCloseText: {
title: Config.exeConfig.name,
defaultId: 0,
cancelId: 0,
message: "确定要关闭吗?",
buttons: ["没事", "直接退出"],
},
type: "info",
windowOpts: {
height: 600,
useContentSize: true,
width: 800,
show: true,
resizable: true,
minWidth: 900,
minHeight: 600,
frame: true,
transparent: false,
alwaysOnTop: false,
icon: icon,
title: Config.exeConfig.name,
webPreferences: {
devTools: false,
sandbox: true,
nodeIntegration: false,
contextIsolation: true,
webviewTag: false,
preload: undefined,
},
},
},
}
}

30
src/main/utils/base/base-singleton.ts

@ -0,0 +1,30 @@
// 抽象基类,使用泛型来正确推导子类类型
abstract class BaseSingleton {
private static _instance: any
public constructor() {
if (this.constructor === BaseSingleton) {
throw new Error("禁止直接实例化 BaseOne 抽象类")
}
if ((this.constructor as any)._instance) {
throw new Error("构造函数私有化失败,禁止重复 new")
}
// this.constructor 是子类,所以这里设为 instance
;(this.constructor as any)._instance = this
}
abstract init(): void
public static getInstance<T extends BaseSingleton>(this: new () => T): T {
const clazz = this as any as typeof BaseSingleton
if (!clazz._instance) {
clazz._instance = new this()
clazz._instance.init()
}
return clazz._instance as T
}
}
export { BaseSingleton }

18
src/main/utils/file.ts

@ -0,0 +1,18 @@
import { is } from "@electron-toolkit/utils"
import { join } from "path"
import { slash } from "./url"
export function getFileUrl(app: string) {
let winURL = ""
if (is.dev && process.env["ELECTRON_RENDERER_URL"]) {
winURL = process.env["ELECTRON_RENDERER_URL"] + `/${app}#/`
} else {
winURL = join(__dirname, `../renderer/${app}#/`)
}
return slash(winURL)
}
export function getPreloadUrl(file) {
return join(__dirname, `../preload/${file}.js`)
}

8
src/main/utils/url.ts

@ -0,0 +1,8 @@
export function slash(path: string) {
const isExtendedLengthPath = path.startsWith("\\\\?\\")
if (isExtendedLengthPath) {
return path
}
return path.replace(/\\/g, "/")
}

27
src/preload/index.ts

@ -1,26 +1 @@
import { contextBridge } from 'electron'
import { electronAPI } from '@electron-toolkit/preload'
import { exposeElectronTRPC } from 'electron-trpc/main';
process.once('loaded', async () => {
exposeElectronTRPC();
});
// Custom APIs for renderer
const api = {}
// Use `contextBridge` APIs to expose Electron APIs to
// renderer only if context isolation is enabled, otherwise
// just add to the DOM global.
if (process.contextIsolated) {
try {
contextBridge.exposeInMainWorld('electron', electronAPI)
contextBridge.exposeInMainWorld('api', api)
} catch (error) {
console.error(error)
}
} else {
// @ts-ignore (define in dts)
window.electron = electronAPI
// @ts-ignore (define in dts)
window.api = api
}
import 'trpc/preload';

32
src/renderer/about.html

@ -0,0 +1,32 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<style>
html,
body {
height: 100%;
width: 100%;
margin: 0;
padding: 0;
outline: none;
border: 0;
overflow: hidden;
padding: 10px 20px;
}
</style>
</head>
<body>
<article>
<h1>关于</h1>
<ul>
<li>MIT开源</li>
</ul>
</article>
<script type="module" src="/src/about.ts"></script>
</body>
</html>

13
src/renderer/src/App.vue

@ -1,18 +1,20 @@
<script setup lang="ts">
import Versions from './components/Versions.vue'
import { client } from './trpc'
import { client } from 'trpc/renderer'
console.log(client);
const ipcHandle = (): void => window.electron.ipcRenderer.send('ping')
client.subscript.subscribe(undefined, {
client.subscribeGreeting.subscribe(undefined, {
onData: (data) => {
console.log(data);
alert(data.text);
}
})
const rpcHandle = async () => {
console.log(await client.greeting.query({ name: 'World' }));
const list = await client.userList.query()
alert(JSON.stringify(list));
}
</script>
@ -33,6 +35,7 @@ const rpcHandle = async () => {
</div>
<div class="action">
<a target="_blank" rel="noreferrer" @click="rpcHandle">Send IPC</a>
<a target="_blank" rel="noreferrer" @click="ipcHandle">About</a>
</div>
</div>
<Versions />

8
src/renderer/src/about.ts

@ -0,0 +1,8 @@
import { client } from 'trpc/renderer'
client.subscribeGreeting.subscribe(undefined, {
onData: (data) => {
console.log(data.text);
}
})

22
tsconfig.node.json

@ -1,8 +1,26 @@
{
"extends": "@electron-toolkit/tsconfig/tsconfig.node.json",
"include": ["electron.vite.config.*", "src/main/**/*", "src/preload/**/*"],
"include": [
"electron.vite.config.*",
"src/main/**/*",
"src/preload/**/*",
"packages/**/common/**/*",
"packages/**/main/**/*",
"packages/**/preload/**/*"
],
"compilerOptions": {
"composite": true,
"types": ["electron-vite/node"]
"types": [
"electron-vite/node"
],
"baseUrl": ".",
"paths": {
"main/*": [
"src/main/*"
],
"@res": [
"resources/*"
]
}
}
}

3
tsconfig.web.json

@ -5,7 +5,8 @@
"src/renderer/src/**/*",
"src/renderer/src/**/*.vue",
"src/preload/*.d.ts",
"src/main/api.ts"
"packages/**/common/**/*",
"packages/**/renderer/**/*"
],
"compilerOptions": {
"composite": true,

Loading…
Cancel
Save