Browse Source

feat: 应用多窗口交互完善
1. 实现窗口控制, 前后端钩子函数

kindring 11 months ago
parent
commit
dad61c4113

File diff suppressed because it is too large
+ 359 - 134
package-lock.json


+ 10 - 6
package.json

@@ -3,20 +3,24 @@
   "private": true,
   "version": "0.0.0",
   "scripts": {
-    "dev": "vite",
+    "dev": "chcp 65001 && vite",
     "build": "vue-tsc && vite build",
     "preview": "vite preview",
-    "rebuild": "electron-rebuild -f -w better-sqlite3"
+    "rebuild": "electron-rebuild --force -w better-sqlite3 ",
+    "rebuild-better-sqlite3": "cd node_modules/better-sqlite3 && node-gyp rebuild --release --build-from-source --runtime=electron --target=26.0.0 --dist-url=https://electronjs.org/headers\n"
   },
   "devDependencies": {
     "@types/better-sqlite3": "^7.6.10",
     "@vitejs/plugin-vue": "^4.2.3",
-    "electron-builder": "^24.6.4",
+    "@types/express": "^4.17.21",
+    "express": "^4.19.2",
+    "log4js": "^6.9.1",
+    "better-sqlite3": "^10.0.0",
+    "electron": "^26.0.0",
+    "electron-builder": "^24.13.3",
     "electron-rebuild": "^3.2.9",
-    "knex": "^3.1.0",
-    "better-sqlite3": "^9.6.0",
-    "electron": "^30.0.3",
     "fs-extra": "^11.2.0",
+    "knex": "^3.1.0",
     "typescript": "^5.0.2",
     "vite": "^4.4.5",
     "vite-plugin-optimizer": "^1.4.2",

+ 21 - 15
plugins/buildPlugin.ts

@@ -14,11 +14,25 @@ class BuildObj {
             external: ["electron", "pg", "tedious", "mysql", "mysql2", "oracledb", "pg-query-stream", "sqlite3"],
         });
     }
+    //为生产环境准备package.json
+    preparePackageJson() {
+        let pkgJsonPath = path.join(process.cwd(), "package.json");
+        let localPkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, "utf-8"));
+        let electronConfig = localPkgJson.devDependencies.electron.replace("^", "");
+        localPkgJson.main = "mainEntry.js";
+        delete localPkgJson.scripts;
+        delete localPkgJson.devDependencies;
+        localPkgJson.devDependencies = { electron: electronConfig };
+        let tarJsonPath = path.join(process.cwd(), "dist", "package.json");
+        fs.writeFileSync(tarJsonPath, JSON.stringify(localPkgJson));
+        fs.mkdirSync(path.join(process.cwd(), "dist/node_modules"));
+    }
     async prepareSqlite() {
         //拷贝better-sqlite3
         let srcDir = path.join(process.cwd(), `node_modules/better-sqlite3`);
         let destDir = path.join(process.cwd(), `dist/node_modules/better-sqlite3`);
         fs.ensureDirSync(destDir);
+        console.log(srcDir, destDir);
         fs.copySync(srcDir, destDir, {
             filter: (src) => {
                 if (src.endsWith("better-sqlite3") || src.endsWith("build") || src.endsWith("Release") || src.endsWith("better_sqlite3.node")) return true;
@@ -26,6 +40,11 @@ class BuildObj {
                 else return false;
             },
         });
+        // 拷贝build目录
+        srcDir = path.join(process.cwd(), `node_modules/better-sqlite3/build`);
+        destDir = path.join(process.cwd(), `dist/build`);
+        fs.ensureDirSync(destDir);
+        fs.copySync(srcDir, destDir);
 
         let pkgJson = `{"name": "better-sqlite3","main": "lib/index.js"}`;
         let pkgJsonPath = path.join(process.cwd(), `dist/node_modules/better-sqlite3/package.json`);
@@ -60,19 +79,7 @@ class BuildObj {
         fs.writeFileSync(pkgJsonPath, pkgJson);
     }
 
-    //为生产环境准备package.json
-    preparePackageJson() {
-        let pkgJsonPath = path.join(process.cwd(), "package.json");
-        let localPkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, "utf-8"));
-        let electronConfig = localPkgJson.devDependencies.electron.replace("^", "");
-        localPkgJson.main = "mainEntry.js";
-        delete localPkgJson.scripts;
-        delete localPkgJson.devDependencies;
-        localPkgJson.devDependencies = { electron: electronConfig };
-        let tarJsonPath = path.join(process.cwd(), "dist", "package.json");
-        fs.writeFileSync(tarJsonPath, JSON.stringify(localPkgJson));
-        fs.mkdirSync(path.join(process.cwd(), "dist/node_modules"));
-    }
+
     //使用electron-builder制成安装包
     buildInstaller() {
         let options = {
@@ -108,12 +115,11 @@ export let buildPlugin = () => {
         name: "build-plugin",
         closeBundle: () => {
             let buildObj = new BuildObj();
-            buildObj.prepareSqlite();
             buildObj.buildMain();
             buildObj.preparePackageJson();
+            buildObj.prepareSqlite();
             buildObj.buildInstaller();
             buildObj.prepareKnexjs();
-
         },
 
     };

+ 23 - 5
plugins/devPlugin.ts

@@ -1,4 +1,7 @@
 import { ViteDevServer } from "vite";
+
+
+
 export let devPlugin = () => {
     return {
         name: "dev-plugin",
@@ -10,20 +13,34 @@ export let devPlugin = () => {
                 outfile: "./dist/mainEntry.js",
                 external: ["electron", "pg", "tedious", "mysql", "mysql2", "oracledb", "pg-query-stream", "sqlite3"],
             });
-            // 修复 'server.httpServer' is possibly 'null'. 的问题
-            if(!server.httpServer) throw new Error("server.httpServer is null check devPlugin.ts");
+            // require("esbuild").buildSync({
+            //     entryPoints: ["./src/main/preload.ts"],
+            //     bundle: true,
+            //     platform: "node",
+            //     outfile: "./dist/preload.js",
+            //     external: ["electron", "pg", "tedious", "mysql", "mysql2", "oracledb", "pg-query-stream", "sqlite3"],
+            // });
+
+            if (!server.httpServer) throw new Error("server.httpServer is null check devPlugin.ts  ");
+
             server.httpServer?.once("listening", () => {
                 let { spawn } = require("child_process");
                 let addressInfo = server.httpServer?.address() as any;
+                // console.log(server);
+                // console.log(addressInfo);
+
                 let httpAddress = `http://${addressInfo.address}:${addressInfo.port}`;
+
                 let electronProcess = spawn(require("electron").toString(), ["./dist/mainEntry.js", httpAddress], {
                     cwd: process.cwd(),
                     stdio: "inherit",
                 });
+
                 electronProcess.on("close", () => {
                     server.close();
                     process.exit();
                 });
+
             });
         },
     };
@@ -32,15 +49,13 @@ export let devPlugin = () => {
 
 export let getReplacer = () => {
     let externalModels = ["os", "fs", "path", "events", "child_process", "crypto", "http", "buffer", "url", "better-sqlite3", "knex"];
-    // let result = {};
-    let result: { [key: string]: () => { find: RegExp, code: string } } = {};
+    let result = {};
     for (let item of externalModels) {
         result[item] = () => ({
             find: new RegExp(`^${item}$`),
             code: `const ${item} = require('${item}');export { ${item} as default }`,
         });
     }
-    // if(!result["electron"]) throw new Error("getReplacer() electron not exists");
     result["electron"] = () => {
         let electronModules = ["clipboard", "ipcRenderer", "nativeImage", "shell", "webFrame"].join(",");
         return {
@@ -50,3 +65,6 @@ export let getReplacer = () => {
     };
     return result;
 };
+
+
+

+ 4 - 4
src/App.vue

@@ -1,11 +1,9 @@
 <script setup lang="ts">
 import HelloWorld from './components/HelloWorld.vue'
-import fs from "fs";
-import { ipcRenderer } from "electron";
+
 import { onMounted } from "vue";
 onMounted(() => {
-  console.log(fs.writeFileSync);
-  console.log(ipcRenderer);
+
 });
 </script>
 
@@ -28,9 +26,11 @@ onMounted(() => {
   will-change: filter;
   transition: filter 300ms;
 }
+
 .logo:hover {
   filter: drop-shadow(0 0 2em #646cffaa);
 }
+
 .logo.vue:hover {
   filter: drop-shadow(0 0 2em #42b883aa);
 }

+ 113 - 0
src/common/appConfig.ts

@@ -0,0 +1,113 @@
+import {app} from "electron";
+import Path from "path";
+
+import Logger from '../util/logger';
+import fs from "fs";
+import path from "path";
+
+
+let logger = Logger.logger('config', 'info');
+const appPath = app.isPackaged ? Path.dirname(app.getPath('exe')) : app.getAppPath();
+
+const configPath = Path.resolve( appPath,`configs/fc-ele.json`);
+logger.info(`[config app] configPath: ${configPath}`);
+
+const defaultConfig : AppConfig = {
+    // 项目高级配置文件 使用nedb存储至用户目录
+    dbPath: '',
+    // 退出时是否提示
+    exitQuestion: true,
+    // 直接退出或者缩小到托盘
+    exitMode: 'normal',
+    // 快捷键
+    hotKey: {
+        // 显示
+        show: 'ctrl+alt+h',
+        min: 'ctrl+alt+x',
+    },
+    // 窗口队列
+    saveWinSize: 1,
+    enableIpv6: true,
+};
+
+let _config: null|AppConfig = null;
+
+// 加载项目配置文件 json
+function _loadProjectConfig(): null|AppConfig {
+    let config: null|AppConfig = null;
+    try {
+        let data = fs.readFileSync(configPath, 'utf-8');
+        // fs 的 buffer 转字符串
+        config = JSON.parse(data.toString());
+    } catch (err) {
+        logger.error(err);
+    }
+    return config;
+}
+
+function _saveProjectConfig(config: AppConfig): boolean {
+    try {
+        // 判断目录是否存在
+        if(!fs.existsSync(path.dirname(configPath))){
+            logger.info("创建默认配置文件");
+            fs.mkdirSync(path.dirname(configPath));
+        }
+        fs.writeFileSync(configPath, JSON.stringify(config));
+        return true;
+    } catch (err) {
+        logger.error(err);
+        logger.info("save config file fail")
+        return false;
+    }
+}
+
+// 修改配置文件
+// function _changeConfig(key: string, value: any): boolean {
+//     if(!_config){
+//         return false;
+//     }
+//     _config = {
+//     ..._config,
+//         [key]: value,
+//     };
+//     return _saveProjectConfig(_config);
+// }
+
+
+export function saveConfig(appConfig: AppConfig): boolean {
+    return _saveProjectConfig(appConfig);
+}
+
+export function getProjectConfig() : Promise<AppConfig> {
+    return new Promise((resolve, reject) => {
+        let config = _loadProjectConfig();
+        if(config){
+            resolve(config);
+        }else{
+            reject()
+        }
+    })
+}
+
+
+
+export function initData(): AppConfig {
+    let config = _loadProjectConfig();
+    if(!config){
+        logger.info("init config");
+        config = defaultConfig;
+    }
+    // 合并config
+    _config = {
+        ...defaultConfig,
+        ...config,
+    };
+    // 保存配置文件
+    _saveProjectConfig(_config);
+
+    // 创建数据库
+    return _config;
+}
+
+
+

+ 9 - 3
src/main.ts

@@ -1,5 +1,11 @@
-import { createApp } from 'vue'
+import {App, createApp} from 'vue'
 import './style.css'
-import App from './App.vue'
+import AppPage from './App.vue'
+import {windowInit} from "./util/pageHandle.ts";
 
-createApp(App).mount('#app')
+
+const app: App = createApp(AppPage)
+
+windowInit(app);
+
+app.mount('#app');

+ 269 - 0
src/main/AppControl.ts

@@ -0,0 +1,269 @@
+import Logger from "../util/logger";
+import {BrowserWindow} from "electron";
+import {CustomScheme} from "./CustomScheme";
+import {getAvailablePort} from "./tools/port.ts";
+import {FcServer, startServer} from "./server/httpServer.ts";
+import {AppWindow, AppConfig} from "../types/appConfig.ts";
+import {randomId} from "../util/random.ts";
+import {actionMap} from "../tools/IpcCmd.ts";
+import BrowserWindowConstructorOptions = Electron.BrowserWindowConstructorOptions;
+import {initIpc} from "./tools/ipcInit.ts";
+import {initHook} from "./tools/hookInit.ts";
+
+let logger = Logger.logger('controlWindow', 'info');
+
+let WebPort = 21000;
+let BaseUrl = "http://127.0.0.1";
+
+let _app: Electron.App | null = null;
+let _webServer: FcServer | null = null;
+let _appConfig: AppConfig;
+let _winArr: AppWindow[] = [];
+let checkTimer: NodeJS.Timeout;
+let isExitAppTask = false;// 是否处于退出任务中
+function _generate_unique_window_id(){
+    let id = randomId();
+    let ind = -1;
+    let t = 0;
+    // eslint-disable-next-line no-constant-condition
+    while(true){
+        t++;
+        id = randomId();
+        ind = _winArr.findIndex(value => value.id === id);
+        if(ind === -1){
+            return id;
+        }
+        if(t>=10){
+            return id + _winArr.length;
+        }
+    }
+}
+
+function findWin(sign: string): AppWindow | null {
+    let ind = _winArr.findIndex(value => value.sign === sign);
+    if (ind !== -1) {
+        return _winArr[ind];
+    }
+    return null;
+}
+
+
+function removeWin(sign: string){
+    let winObj = findWin(sign);
+    if(winObj && winObj.win){
+        if(winObj.type === 'top'){
+            // todo 从topWinArr 中移除窗口对象
+            // exitTopWin(winObj);
+            logger.info(`窗口${winObj.sign} 已经从topWinArr 中移除`);
+        }
+        winObj.win.destroy();
+        winObj.win = null;
+        _winArr.splice(_winArr.indexOf(winObj),1);
+        return true;
+    }
+    return false;
+}
+
+
+
+/**
+ * 遍历绑定窗口的句柄
+ */
+function winTryConnect(): void {
+    if (checkTimer) {
+        clearTimeout(checkTimer);
+        // 清除计时器
+    }
+
+    checkTimer = setTimeout(() => {
+        let connectedTotal = 0;
+        let i = 0;
+        for (let j = 0; j < _winArr.length; j++) {
+            let item = _winArr[j];
+            i++;
+            if (!item.isConnected) {
+                // console.log(item);
+                logger.info(`正在连接窗口${i}/${_winArr.length}, sign=${item.sign}, connectedTotal=${connectedTotal}`);
+                if (item.win) {
+                    item.win.webContents.send(actionMap.bindSignId.code, {
+                        signId: item.sign,
+                        baseUrl: `${BaseUrl}:${WebPort}`,
+                        key: _webServer? _webServer.$serverKey : '',
+                    });
+                } else {
+                    logger.error(`窗口 ${item.sign} 的窗口对象不存在`);
+                }
+            } else {
+                connectedTotal++;
+            }
+        }
+        if (connectedTotal === _winArr.length) {
+            logger.info("窗口已经全部连接完成");
+            clearTimeout(checkTimer);
+        } else {
+            winTryConnect();
+        }
+    }, 500);
+}
+
+
+/** 绑定app方便在这里面进行退出操作 */
+function registerApp(newApp: Electron.App) {
+    _app = newApp;
+}
+
+function registerWin(windowConfig: AppWindow): AppWindow{
+    let defaultWin : AppWindow = {
+        id: '',
+        sign: '',
+        parentSign: '',
+        type: '',
+        title: '未知窗口',
+        description: '窗口描述文件',
+        win: null,
+        isMain: false,
+        timer: null,// 等待销毁计时器
+        hide: false,// 是否隐藏
+        isConnected: false,// 是否已经建立连接
+        isUsed: false,// 是否被使用中,用于复用窗口
+        destroyWait: 30,
+        style: {
+            width: 0,
+            height: 0,
+            x: 0,
+            y: 0
+        }
+    }
+    let finalWindow = {...defaultWin, ...windowConfig }
+    finalWindow.id = _generate_unique_window_id();
+    finalWindow.sign = `${finalWindow.type}:${finalWindow.id}`;
+
+    // 窗口挂载
+    if (!finalWindow.win || !finalWindow.sign) {
+        //窗口挂载成功
+        logger.error(`窗口挂载失败,窗口标记:${finalWindow.sign},窗口数量${_winArr.length}`);
+    }
+
+    _winArr.push(finalWindow);
+    logger.info(`窗口挂载成功,窗口标记:${finalWindow.sign},窗口数量${_winArr.length}`);
+
+    winTryConnect();
+
+
+
+    return finalWindow;
+}
+
+function _createMainWindow(){
+    let MainUrl : string = `app://index.html`
+    // let  preloadPath = path.join(__dirname, 'preload.js');
+    if (process.argv[2]) {
+        console.log(process.argv[2])
+        MainUrl = process.argv[2];
+    } else {
+        CustomScheme.registerScheme();
+        MainUrl = `app://index.html`
+    }
+    let config: BrowserWindowConstructorOptions = {
+        width: 1000,
+        height: 620,
+        frame: false, //任务栏是否显示
+        show: true,
+        transparent: false, //无边框
+        resizable: false,// 禁止 重新设置窗口大小
+        maximizable: false, //禁止最大化
+        webPreferences: {
+            nodeIntegration: true,
+            webSecurity: false,
+            allowRunningInsecureContent: true,
+            contextIsolation: false,
+            webviewTag: true,
+            spellcheck: false,
+            disableHtmlFullscreenWindowResize: true,
+        },
+    };
+    let mainWindow = new BrowserWindow(config);
+    mainWindow.loadURL(MainUrl);
+    mainWindow.webContents.openDevTools();
+    return mainWindow;
+}
+export async function initApp(appConfig: AppConfig, app: Electron.App) : Promise<AppWindow | null>{
+    logger.info('start init control window');
+    let mainWindow : BrowserWindow = _createMainWindow();
+    let err, port: number, server: FcServer | null;
+    _appConfig = appConfig;
+    [err,port] = await getAvailablePort(WebPort,300);
+    if (port === -1){
+        logger.error(`[应用初始化] 获取可用端口失败`);
+        return null
+    }
+    WebPort = port;
+    logger.info(`get allow webPort: ${WebPort}`);
+    // 启动web服务
+    [err, server] = await startServer(WebPort, _appConfig.enableIpv6);
+    if (err){
+        logger.error(`[应用初始化] 启动web服务失败: ${err}`);
+        return null
+    }
+    logger.info(`[应用初始化] 启动web服务成功`);
+    _webServer = server;
+
+    // 初始化钩子函数
+    initHook();
+    // 初始化 Ipc 监听
+    initIpc();
+
+    // 创建主窗口
+    let mainWin = registerWin({
+        type: 'main',
+        title: '主进程窗口',
+        win: mainWindow,
+        isMain: true,
+    });
+
+    // 绑定主进程
+    registerApp(app)
+
+
+
+    return mainWin;
+}
+
+
+async function exit(){
+    logger.info(`[应用退出] 应用退出中....`);
+    if(!_app){
+        logger.error(`[应用退出] 无法找到主应用. 非常离奇的情况, 按理说不应该这样的`);
+        return 0;
+    }
+    isExitAppTask = true;
+    // fixme: 修复退出软件时,窗口不会关闭的问题. 以及引用问题
+    while(_winArr.length > 0){
+        let winObj = _winArr.pop() as AppWindow;
+        if(!winObj.win){
+            logger.error(`[应用退出] 无法找到窗口对象, 需要修复呢`);
+            continue;
+        }
+        // if(winObj.type === 'top'){
+        //     // 从topWinArr 中移除窗口对象
+        //     exitTopWin(winObj);
+        // }
+        // 移除所有win的监听事件
+        winObj.win.removeAllListeners();
+        winObj.win.close();
+        winObj.win.destroy();
+        winObj.win = null;
+    }
+    console.log(`退出软件`);
+    // 清理窗口
+    _app.quit();
+    return 0;
+}
+
+
+export default {
+    isExitAppTask,
+    findWin,
+    removeWin,
+    exit
+}

+ 98 - 39
src/main/mainEntry.ts

@@ -1,41 +1,100 @@
 
-import { app, BrowserWindow } from "electron";
-import {CustomScheme} from "./CustomScheme.ts";
-
-import { db } from "../common/db/db.ts";
-
-
-let mainWindow: BrowserWindow;
-
-app.whenReady().then(() => {
-    let config = {
-        webPreferences: {
-            nodeIntegration: true,
-            webSecurity: false,
-            allowRunningInsecureContent: true,
-            contextIsolation: false,
-            webviewTag: true,
-            spellcheck: false,
-            disableHtmlFullscreenWindowResize: true,
-        },
-    };
-    mainWindow = new BrowserWindow(config);
-    if (process.argv[2]) {
-        mainWindow.loadURL(process.argv[2]);
-    } else {
-        CustomScheme.registerScheme();
-        mainWindow.loadURL(`app://index.html`);
+import { app } from "electron";
+
+import Logger from "../util/logger.ts";
+import {initData} from "../common/appConfig.ts";
+import {initApp} from "./AppControl.ts";
+import {AppWindow} from "../types/appConfig.ts";
+
+
+let mainWindow: AppWindow | null;
+
+const isDevelopment = process.env.NODE_ENV !== 'production'
+
+let logger = Logger.logger('background.js', 'info');
+const gotTheLock = app.requestSingleInstanceLock();
+if (!gotTheLock) {
+    logger.info("[fc-ele] 应用已经启动");
+    app.quit();
+}else {
+    let version = app.getVersion();
+    logger.info(`[fc-ele] ---------file Control for electron (FC-ELE):${version}---------`);
+    // event listen
+    app.on('window-all-closed', handle_windowAllClosed);
+    app.on('activate', handle_activate)
+    app.whenReady().then(handle_ready).catch(
+        (e) => {
+            logger.error("[fc-ele] app whenReady error", e.toString());
+        }
+    )
+}
+
+function handle_windowAllClosed(){
+    // On macOS it is common for applications and their menu bar
+    // to stay active until the user quits explicitly with Cmd + Q
+    if (process.platform !== 'darwin') {
+        app.quit()
+    }
+}
+
+function handle_activate(){
+    // 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 (mainWindow?.win === null) {
+        logger.info("[fc-ele] app activate")
+        startApp();
+    }
+}
+
+function handle_ready(){
+    if (isDevelopment && !process.env.IS_TEST) {
+        // Install Vue Devtools
+        // try {
+        //     await installExtension(VUEJS_DEVTOOLS)
+        // } catch (e) {
+        //     console.error('Vue Devtools failed to install:', e.toString())
+        // }
     }
-    // 开启控制台
-    mainWindow.webContents.openDevTools();
-    db("User")
-        .first()
-        .then((obj) => {
-            console.log(obj);
-        }).catch((err) => {
-        console.log("err")
-            console.log(err);
-        });
-
-
-});
+    logger.info("[fc-ele] app ready")
+    startApp();
+}
+
+async function startApp(){
+    let appConfig = initData();
+    logger.info(`配置文件加载完成,${JSON.stringify(appConfig)}`);
+    //
+    mainWindow = await initApp(appConfig, app);
+    if(!mainWindow || !mainWindow.win){
+        app.quit();
+        return;
+    }
+    mainWindow.win.on('closed', () => {
+        mainWindow!.win = null
+    });
+}
+
+
+//
+// app.whenReady().then(() => {
+//     let config = {
+//         webPreferences: {
+//             nodeIntegration: true,
+//             webSecurity: false,
+//             allowRunningInsecureContent: true,
+//             contextIsolation: false,
+//             webviewTag: true,
+//             spellcheck: false,
+//             disableHtmlFullscreenWindowResize: true,
+//         },
+//     };
+//     mainWindow = new BrowserWindow(config);
+//     if (process.argv[2]) {
+//         console.log(process.argv[2])
+//         mainWindow.loadURL(process.argv[2]);
+//     } else {
+//         CustomScheme.registerScheme();
+//         mainWindow.loadURL(`app://index.html`);
+//     }
+//     // 开启控制台
+//     mainWindow.webContents.openDevTools();
+// });

+ 3 - 0
src/main/preload.ts

@@ -0,0 +1,3 @@
+import { ipcRenderer } from 'electron'
+
+export default ipcRenderer

+ 78 - 0
src/main/server/httpServer.ts

@@ -0,0 +1,78 @@
+import express, {Request, Response, NextFunction, Express} from 'express';
+import Logger from "../../util/logger.ts";
+import {createKey} from "../../util/expireKey.ts";
+import r_index from "./router/r_index.ts";
+import http from "http";
+
+let logger = Logger.logger('httpServer', 'info');
+
+export interface FcServer extends http.Server {
+    $serverKey: string;
+    stop: () => void;
+}
+let app: Express = express();
+let _server: FcServer | null = null;
+// 监听地址,默认监听ipv4
+let serverHost = '0.0.0.0';
+
+
+
+/**
+ * 启动web服务器
+ * @param port
+ * @returns {Promise<unknown>}
+ */
+export function startServer(port: number, enableIpv6 = true) : Promise<[Error | null , FcServer | null]> {
+    let key = createKey();
+    return new Promise((resolve) => {
+        if(enableIpv6){
+            serverHost = '::';
+        }
+        // 允许跨域
+        app.all('*', function (_req: Request, res: Response, next: NextFunction ) {
+            res.header("Access-Control-Allow-Origin", "*");
+            res.header("Access-Control-Allow-Headers", "*");
+            res.header("Access-Control-Allow-Methods", "*");
+            res.header("Content-Type", "application/json;charset=utf-8");
+            next();
+        });
+        app.use((req: Request, _res: Response, next: NextFunction)=>{
+            // 记录请求日志, 相应状态, 请求方法, 请求地址
+            logger.info(`[ServerRouter] ${req.method} ${req.url}`);
+            next();
+        });
+        // 挂载路由等中间件
+        logger.info(`[server] 挂载路由等中间件`);
+        app.use(r_index);
+        // 未知路由处理
+        app.use((req: Request, res: Response)=>{
+            logger.warn(`[ServerRouter] 未知请求 ${req.method} ${req.url} ${res.statusCode} queryIp:${req.ip}`);
+            res.status(404).send('404');
+        });
+        let server: http.Server = app.listen(port, serverHost, () => {
+            logger.info(`[server] server start at port:${port}`);
+            let fc_server: FcServer = server as FcServer;
+            fc_server.$serverKey = key;
+            fc_server.stop = stopServer;
+            resolve([null, fc_server]);
+        });
+        _server = server as FcServer;
+        _server.$serverKey = key;
+        _server.stop = stopServer;
+        server.on('error', (err: Error) => {
+            logger.error(`[server] server start error:${err}`);
+            _server = null;
+            resolve([err, null])
+        });
+    });
+}
+
+async function stopServer() {
+    if(_server){
+        _server.close();
+        _server = null;
+    }else{
+        logger.warn('server is not running');
+    }
+}
+

+ 17 - 0
src/main/server/middleware/keyCheck.ts

@@ -0,0 +1,17 @@
+import { Request, Response, NextFunction} from 'express';
+import {notPermission} from "../../../util/httpResult.ts";
+import expireKey from "../../../util/expireKey.ts";
+/**
+ * 用于检查请求中是否携带key,以及其key是否合法
+ * @param req
+ * @param res
+ * @param next
+ */
+export default function (req: Request, res: Response, next: NextFunction) {
+    let key = req.headers['key'];
+    if(!key || !expireKey.checkKey(key)){
+        return notPermission(res, 'key error');
+    }
+    expireKey.updateExpire(key);
+    next();
+}

+ 8 - 0
src/main/server/router/r_index.ts

@@ -0,0 +1,8 @@
+import {Request, Response, Router} from "express";
+import keyCheck from "../middleware/keyCheck";
+const router: Router = Router();
+router.get('/test', keyCheck, (req: Request, res: Response) => {
+    res.send(`this is a test ${req.query.name}`);
+});
+
+export default router;

+ 301 - 0
src/main/tools/doWindowAction.ts

@@ -0,0 +1,301 @@
+import AppControl from "../AppControl.ts";
+import Logger from "../../util/logger.ts";
+import {AppWindow} from "../../types/appConfig.ts";
+
+let logger = Logger.logger('doWindowAction', 'info');
+
+export async function connectedWin(sign: string){
+    let winObj = AppControl.findWin(sign);
+    if(winObj){
+        logger.info(`[窗口挂载] 窗口连接成功:${winObj.title}`);
+        winObj.isConnected = true;
+    }
+}
+
+
+/**
+ * 关闭指定窗口
+ * @param {String} sign 窗口标记
+ */
+export function closeWin(sign: string): Promise<boolean> {
+    return new Promise((resolve, reject) => {
+        if(AppControl.isExitAppTask){
+            logger.info('退出任务中,不允许关闭窗口');
+            return reject(Error(`[关闭窗口] 窗口${sign}不存在`));
+        }
+        let winObj = AppControl.findWin(sign);
+        if(!winObj || !winObj.win){
+            logger.info(`[关闭窗口] 窗口${sign}不存在`);
+            return reject(Error(`[关闭窗口] 窗口${sign}不存在`));
+        }
+        try {
+            if(winObj.isMain){
+                return _tryCloseMain(winObj);
+            }
+            logger.info(`[关闭窗口] 窗口{${sign}} 进入准备移除阶段`);
+            // 先隐藏窗口,等待1分钟关闭后再进行删除
+            winObj.hide = true;
+            winObj.win.hide();
+            // 多次点击时可能无法正常清理定时器
+            if(winObj.timer) clearTimeout(winObj.timer);
+            winObj.timer = setTimeout(
+                ()=>{
+                    logger.info(`[关闭窗口] 开始移除{${sign}}占用`);
+                    if(!winObj || !winObj.win) return logger.warn(`[关闭窗口] 移除窗口{${sign}}时,窗口对象已经被销毁`);
+                    logger.info(`[关闭窗口] 移除窗口${sign}监听事件`);
+                    // 移除窗口监听事件
+                    winObj.win.webContents.removeAllListeners();
+                    winObj.win.removeAllListeners();
+                    if(winObj.win.isDestroyed()) return ;
+                    winObj.win.close();
+                    winObj.win.destroy();
+                    let flag = AppControl.removeWin(sign);
+                    if(!flag){
+                        logger.warn(`[关闭窗口] 移除窗口${sign}失败`);
+                        return resolve(false);
+                    }
+                    logger.info(`[关闭窗口] 清理窗口${sign}资源完成`);
+                    resolve(true);
+                },
+                winObj.destroyWait ?? 5 * 1000
+            );
+        } catch (error) {
+            logger.error(error);
+            logger.info('关闭窗口失败');
+            reject(error);
+        }
+    });
+}
+
+export function minWin(sign: string) {
+    return new Promise((resolve, reject) => {
+        let winObj = AppControl.findWin(sign);
+        if(!winObj || !winObj.win) return reject(Error(`[最小化窗口] 窗口${sign}不存在`));
+        try {
+            winObj.win.minimize();
+            logger.debug('最小化窗口: ' + winObj.title);
+            resolve(true);
+        } catch (error) {
+            logger.error('最小化窗口失败');
+            reject(error);
+        }
+    });
+}
+
+/**
+ * 最大化窗口或者恢复窗口
+ * @param {String} sign 窗口标记
+ *
+*/
+export function maxWin(sign: string): Promise<boolean> {
+    return new Promise((resolve, reject) => {
+        let winObj = AppControl.findWin(sign);
+        if(!winObj || !winObj.win) return reject(Error(`[最大化窗口] 窗口${sign}不存在`));
+        try {
+            if (!winObj.win.isMaximizable()) {
+                let size = winObj.win.getSize();
+                winObj.style = {
+                    width: size[0],
+                    height: size[1]
+                }
+                // 获取位置
+                let pos = winObj.win.getPosition();
+                winObj.style.x = pos[0];
+                winObj.style.y = pos[1];
+                winObj.win.maximize();
+                resolve(true)
+            } else {
+                logger.debug(`窗口${winObj.title}已经最大化`)
+                resolve(false)
+            }
+
+        } catch (error) {
+            reject(error);
+        }
+    });
+}
+
+
+export function unMaxWin(sign: string): Promise<boolean> {
+    return new Promise((resolve,reject)=>{
+        try {
+            let winObj = AppControl.findWin(sign);
+            if(!winObj || !winObj.win) return reject(Error(`[取消最大化窗口] 窗口${sign}不存在`));
+            if(!winObj.style) {
+                logger.info(`[取消最大化窗口] 窗口${sign}没有保存窗口大小 使用默认配置`);
+                winObj.style = {
+                    width: 1000,
+                    height: 700,
+                    x: 0,
+                    y: 0,
+                }
+            }
+            winObj.win.setContentSize(winObj.style.width??1000, winObj.style.height??700);
+            winObj.win.setPosition(winObj.style.x??0, winObj.style.y??0);
+            // winObj.win.center(); // 窗口居中
+            resolve(true);
+        } catch (error) {
+            reject(error);
+        }
+    })
+}
+
+export function restoreWin(sign: string): Promise<boolean> {
+    return new Promise((resolve,reject)=>{
+        let winObj = AppControl.findWin(sign);
+        if(!winObj || !winObj.win) return reject(Error(`[恢复窗口] 窗口${sign}不存在`));
+        try {
+            winObj.win.restore();
+            logger.debug('恢复窗口: ' + winObj.title);
+            resolve(true)
+        } catch (error) {
+            logger.error('恢复窗口失败')
+            reject(error)
+        }
+    })
+}
+
+
+/** 置顶指定窗口 */
+export function topWin(sign: string): Promise<boolean> {
+    return new Promise((resolve,reject)=>{
+        let winObj = AppControl.findWin(sign);
+        if(!winObj || !winObj.win) return reject(Error(`[置顶窗口] 窗口${sign}不存在`));
+        try {
+            if (!winObj.win.isAlwaysOnTop()) {
+                winObj.win.setAlwaysOnTop(true, 'normal', 1);
+                // logger.info('置顶窗口: ' + winObj.title);
+                resolve(true);
+            } else {
+                // logger.info('窗口已经置顶');
+                resolve(false);
+            }
+        } catch (err) {
+            let error = err as Error;
+            logger.info('置顶窗口失败');
+            logger.error(error.message);
+            reject(err);
+        }
+    });
+}
+
+
+export function unTopWin(sign: string): Promise<boolean> {
+    return new Promise((resolve,reject)=>{
+        let winObj = AppControl.findWin(sign);
+        if(!winObj || !winObj.win) return reject(Error(`[取消置顶窗口] 窗口${sign}不存在`));
+        try {
+            winObj.win.setAlwaysOnTop(false);
+            logger.debug('取消置顶:' + winObj.title);
+            resolve(true);
+        } catch (err) {
+            logger.info('取消置顶窗口失败');
+            let error = err as Error;
+            // logger.info('置顶窗口失败');
+            logger.error(error.message);
+            reject(err);
+        }
+    });
+}
+
+
+
+export function hideWin(sign: string){
+    return new Promise((resolve,reject)=>{
+        let winObj = AppControl.findWin(sign);
+        if(!winObj || !winObj.win) return reject(Error(`[隐藏窗口] 窗口${sign}不存在`));
+        try {
+            winObj.hide = true;
+            winObj.win.hide();
+            logger.debug('隐藏窗口:' + winObj.title);
+            resolve(true);
+        } catch (err) {
+            logger.debug('隐藏窗口失败');
+            let error = err as Error;
+            // logger.info('置顶窗口失败');
+            logger.error(error.message);
+            reject(err);
+        }
+    });
+}
+
+export function showWin(sign: string){
+    return new Promise((resolve,reject)=>{
+        let winObj = AppControl.findWin(sign);
+        if(!winObj || !winObj.win) return reject(Error(`[显示窗口] 窗口${sign}不存在`));
+        try {
+            winObj.hide = false;
+            winObj.win.show();
+            logger.debug('显示窗口:' + winObj.title);
+            resolve(true);
+        } catch (err) {
+            logger.debug('显示窗口失败');
+            let error = err as Error;
+            // logger.info('置顶窗口失败');
+            logger.error(error.message);
+            reject(err);
+        }
+    });
+}
+
+
+
+export function centerWin(sign: string){
+    return new Promise((resolve,reject)=>{
+        let winObj = AppControl.findWin(sign);
+        if(!winObj || !winObj.win) return reject(Error(`[居中窗口] 窗口${sign}不存在`));
+        try {
+            winObj.win.center();
+            logger.debug('居中显示窗口:' + winObj.title);
+            resolve(true);
+        } catch (err) {
+            logger.debug('居中显示窗口失败');
+            let error = err as Error;
+            // logger.info('置顶窗口失败');
+            logger.error(error.message);
+            reject(err);
+        }
+    });
+}
+
+
+
+/** 申请退出软件 */
+async function _tryCloseMain(mainWinObj: AppWindow) : Promise<[Error | null, boolean]> {
+    // let err, res;
+    // 判断是隐藏还是退出
+    // 判断是否已经在询问是否退出
+    // if (mainWinObj.isQueryClose) {
+    //     logger.info('已经在询问是否退出了,不多弹窗');
+    //     return [null, false];
+    // }
+    mainWinObj.isQueryClose = true;
+    // [err, res] = await openQueryPage(mainWinObj.sign, {
+    //     type: 'question',
+    //     title: "是否退出应用?",
+    //     okText: "退出",
+    //     cancelText: "缩小到托盘",
+    // });
+    // logger.info("询问是否关闭窗口");
+    // mainWinObj.isQueryClose = false;
+    // if(err){
+    //     logger.error(err);
+    //     return [null,false];
+    // }
+    // if(res){
+    //     if(res.action === 'ok'){
+    //         logger.info('确认关闭窗口');
+    //         exit();
+    //     }else if(res.action === 'cancel'){
+    //         logger.info('隐藏窗口');
+    //         // 隐藏窗口
+    //         hideWin(mainWinObj.sign);
+    //     }else if (res.action === 'close'){
+    //         // 毫无作为
+    //     }
+    // }
+    return [null,true];
+}
+
+
+

+ 51 - 0
src/main/tools/hookInit.ts

@@ -0,0 +1,51 @@
+import {IpcAction, actionMap} from "../../tools/IpcCmd.ts";
+import hook, {HookFn} from "../../util/hook.ts";
+import {
+    closeWin,
+    connectedWin,
+    hideWin,
+    maxWin,
+    minWin,
+    restoreWin,
+    showWin, topWin,
+    unMaxWin,
+    unTopWin
+} from "./doWindowAction.ts";
+import appControl from "../AppControl.ts";
+import Logger from "../../util/logger.ts";
+
+let logger = Logger.logger('ipcInit', 'info');
+
+/**
+ * 绑定钩子
+ * @param action 要触发的 action 值
+ * @param fn 触发的函数
+ * @param bindReplay 是否绑定回复
+ */
+function hookBind(action: IpcAction, fn: HookFn<any>, bindReplay = false){
+    if(bindReplay){
+        hook.addHook(action.resCode, fn, `reply_${action.title}`);
+    }else{
+        hook.addHook(action.code, fn, action.title);
+    }
+}
+
+
+// hookBind(windowAction.openSetting, win.openSettingPage);
+
+
+export function initHook(){
+    logger.info('initHook');
+    hookBind(actionMap.bindSignId, connectedWin,true);
+    hookBind(actionMap.close, closeWin);
+    hookBind(actionMap.min, minWin);
+    hookBind(actionMap.max, maxWin);
+    hookBind(actionMap.unMax, unMaxWin);
+    hookBind(actionMap.ding, topWin);
+    hookBind(actionMap.unDing, unTopWin);
+    hookBind(actionMap.restore, restoreWin);
+    hookBind(actionMap.hide, hideWin);
+    hookBind(actionMap.show, showWin);
+    hookBind(actionMap.exitApp, appControl.exit);
+
+}

+ 49 - 0
src/main/tools/ipcInit.ts

@@ -0,0 +1,49 @@
+import {ipcMain} from "electron";
+import {actionMap, IpcAction, windowAction} from "../../tools/IpcCmd.ts";
+import hook from "../../util/hook.ts";
+import Logger from "../../util/logger.ts";
+import {handle} from "../../util/promiseHandle.ts";
+
+let logger = Logger.logger('ipcInit', 'info');
+function bindAction(action: IpcAction, bindReplay: boolean = false) {
+    let code = bindReplay?action.resCode:action.code;
+    logger.info(`绑定ipc事件:${code}-${action.title}`);
+    ipcMain.on(code, async (_, arg) => {
+        // console.log(event);
+        logger.info(`${code}-${action.title},参数:${arg}`);
+        let [err,res] = await handle(
+            hook.runHook(code,arg)
+        );
+        if(err){
+            logger.error(err);
+        }
+        logger.debug(`${code}-${action.title},返回:${res}`);
+    });
+}
+
+
+export function onceIpcReply(code: string) {
+    return new Promise((resolve, _) => {
+        ipcMain.once(code, (_, arg) => {
+            resolve(arg);
+        });
+    });
+}
+
+
+
+export function initIpc() {
+    logger.info('初始化ipc事件');
+    // 绑定ipc事件
+    bindAction(windowAction.bindSignId,true);
+    bindAction(windowAction.close);
+    bindAction(windowAction.min);
+    bindAction(windowAction.max);
+    bindAction(windowAction.unMax);
+    bindAction(windowAction.ding);
+    bindAction(windowAction.unDing);
+    bindAction(windowAction.restore);
+    bindAction(windowAction.openSetting);
+    bindAction(actionMap.exitApp);
+}
+

+ 60 - 0
src/main/tools/port.ts

@@ -0,0 +1,60 @@
+import net from "net";
+import Logger from "../../util/logger.ts";
+
+const logger = Logger.logger('s_port', 'info');
+export async function portIsOccupied(port: number) : Promise<number> {
+    return new Promise((resolve, reject) => {
+        // 创建服务并监听该端口
+        const server = net.createServer();
+        server.listen(port);
+        server.on('listening', () => {
+            // 执行这块代码说明端口未被占用
+            server.close(); // 关闭服务
+            resolve(port);
+        })
+
+        server.on('error', (err: Error) => {
+            if (err.name === 'EADDRINUSE') {
+                // 端口已经被使用
+                resolve(-1);
+            } else {
+                // 未知异常
+                reject(err)
+            }
+        });
+    })
+}
+
+/**
+ * 获取可用端口
+ * @param port
+ * @param maxTry
+ * @returns 端口号
+ */
+export async function
+getAvailablePort<T>(port: number = 3000, maxTry: number = 100): Promise<[Error | null | T | undefined, number]>
+{
+    let tryCount = 0;
+    let rPort = port;
+    // logger.info(rPort);
+    while (tryCount < maxTry) {
+        try {
+            rPort = await portIsOccupied(rPort);
+            logger.info(rPort);
+            if(rPort != -1){
+                return [null, rPort];
+            }
+            rPort += 1;
+            tryCount += 1;
+            if( maxTry >= 0 && tryCount >= maxTry && rPort > 65535){
+                return [null,-1];
+            }
+        } catch (e) {
+            if (e instanceof Error) {
+                return [e, -1];
+            }
+            return [null, -1];
+        }
+    }
+    return [null,-1];
+}

+ 5 - 0
src/tools/ErrorResult.ts

@@ -0,0 +1,5 @@
+interface Person {
+    name: string;
+    age: number;
+    gender: string;
+}

+ 104 - 0
src/tools/IpcCmd.ts

@@ -0,0 +1,104 @@
+export interface IpcAction {
+    title: string;
+    icon: string;
+    code: string;
+    resCode: string;
+}
+
+
+export const actionMap: { [key: string]: IpcAction } = {
+    min: {
+        title: '最小化',
+        icon: 'minimize',
+        code: 'minWin',
+        resCode: 'minWin_replay'
+    },
+    restore: {
+        title: '恢复窗口',
+        icon: 'restore',
+        code: 'restoreWin',
+        resCode: 'restoreWin_replay'
+    },
+    max: {
+        title: '最大化',
+        icon: 'maximize',
+        code: 'maxWin',
+        resCode: 'maxWin_replay'
+    },
+    unMax: {
+        title: '还原窗口大小',
+        icon: 'restore',
+        code: 'unMaxWin',
+        resCode: 'unMaxWin_replay'
+    },
+    close: {
+        title: '关闭窗口',
+        icon: 'minimize',
+        code: 'closeWin',
+        resCode: 'closeWin_replay'
+    },
+    ding: {
+        title: '置顶',
+        icon: 'top',
+        code: 'topWin',
+        resCode: 'topWin_replay'
+    },
+    unDing: {
+        title: '取消置顶',
+        icon: 'unTop',
+        code: 'unTopWin',
+        resCode: 'unTopWin_replay'
+    },
+    show: {
+        title: '显示窗口',
+        icon: 'show',
+        code: 'showWin',
+        resCode: 'showWin_replay'
+    },
+    hide: {
+        title: '隐藏窗口',
+        icon: 'hide',
+        code: 'hideWin',
+        resCode: 'hideWin_replay'
+    },
+    openSetting: {
+        title: '打开设置页面',
+        icon: 'setting',
+        code: 'openSetting',
+        resCode: 'openSetting_replay'
+    },
+    exitApp: {
+        title: '退出软件',
+        icon: 'top',
+        code: 'exitApp',
+        resCode: 'exitApp_replay'
+    },
+    bindSignId: {
+        title: "绑定窗口句柄",
+        icon: 'connect',
+        code: 'bindSignId',
+        resCode: 'bindSignId_replay'
+    },
+    questionUser: {
+        title: '询问用户弹窗',
+        icon: 'query',
+        code: 'questionUser',
+        resCode: 'userAnswer'
+    }
+}
+
+export const windowAction: { [key: string]: IpcAction } = {
+    min: actionMap.min,
+    restore: actionMap.restore,
+    max: actionMap.max,
+    unMax: actionMap.unMax,
+    close: actionMap.close,
+    ding: actionMap.ding,
+    unDing: actionMap.unDing,
+    show: actionMap.show,
+    hide: actionMap.hide,
+    openSetting: actionMap.openSetting,
+    exitApp: actionMap.exitApp,
+    bindSignId: actionMap.bindSignId,
+    questionUser: actionMap.questionUser
+};

+ 68 - 0
src/types/appConfig.ts

@@ -0,0 +1,68 @@
+import {BrowserWindow} from "electron";
+
+interface HotKeyConfig {
+    show: string;
+    min: string;
+}
+export interface AppConfig {
+    dbPath: string;
+    exitQuestion: boolean;
+    exitMode: string;
+    hotKey: HotKeyConfig;
+    saveWinSize: number;
+    enableIpv6: boolean;
+}
+
+// let defaultWin = {
+//     sign: null,
+//     parentSign: null,
+//     type: '',
+//     title: '未知窗口',
+//     descript: '窗口描述文件',
+//     win: null,
+//     isMain: false,
+//     timer: null,// 等待销毁计时器
+//     hide: false,// 是否隐藏
+//     isConnected: false,// 是否已经建立连接
+//     isUsed: false,// 是否被使用中,用于复用窗口
+//     destroyWait: 30,
+//     style: {
+//         width: 0,
+//         height: 0,
+//         x: 0,
+//         y: 0
+//     }
+// }
+
+export interface AppWindow {
+    isMain: boolean;
+    win: BrowserWindow | null;
+    type: string;
+    title: string;
+    id?: string;
+    sign?: string;
+    parentSign?: string;
+    description?: string;
+    timer?: NodeJS.Timeout | null;// 等待销毁计时器
+    hide?: boolean;// 是否隐藏
+    isConnected?: boolean;// 是否已经建立连接
+    isUsed?: boolean;// 是否被使用中,用于复用窗口
+    destroyWait?: number;
+    isQueryClose?: boolean;// 窗口是否在询问关闭中
+    style?: {
+        width?: number;
+        height?: number;
+        x?: number;
+        y?: number;
+    }
+
+}
+
+
+
+export interface registerWindowData {
+    signId: string;
+    baseUrl: string;
+    key: string;
+}
+

+ 49 - 0
src/util/expireKey.ts

@@ -0,0 +1,49 @@
+interface KeyItem {
+    key: string;
+    time: number;
+}
+
+let keys: KeyItem[] = [];
+let expireTime: number = 1000 * 60 * 10; // 10分钟
+
+// 检查key是否存在
+function checkKey(key: string): boolean {
+    let index = keys.findIndex(item => item.key === key);
+    return index !== -1;
+}
+
+// 生成key
+export function createKey(): string {
+    let key = '';
+    do {
+        key = Math.random().toString(36).substr(2);
+    } while (checkKey(key));
+    keys.push({
+        key,
+        time: Date.now()
+    });
+    return key;
+}
+
+// 检查是否过期
+function checkExpire(): void {
+    let now = Date.now();
+    keys = keys.filter(item => {
+        return now - item.time < expireTime;
+    });
+}
+
+// 更新过期时间
+function updateExpire(key: string): void {
+    let index = keys.findIndex(item => item.key === key);
+    if (index !== -1) {
+        keys[index].time = Date.now();
+    }
+}
+
+export default {
+    checkKey,
+    createKey,
+    checkExpire,
+    updateExpire
+};

+ 137 - 0
src/util/hook.ts

@@ -0,0 +1,137 @@
+// 自定义维护 hook
+// 1. 用于维护自定义的 hook
+
+// 类型定义
+import Logger from "./logger.ts";
+
+// 定义 HookFn 类型
+export type HookFn<T> = (...args: T[]) => Promise<T>;
+
+interface Hook {
+    id: string;
+    tips: string;
+    key: string;
+    hookFn: HookFn<any>;
+}
+
+type HookArray = Hook[];
+
+const logger = Logger.logger('hook.js', 'init');
+
+let hookArr: HookArray = [];
+//
+// let defaultHook: Hook = {
+//     tips: '',
+//     key: '',
+//     hookFn: Promise.resolve(),
+// }
+
+// 随机生成 id
+function randomId(): string {
+    return Math.random().toString(36).substr(4);
+}
+
+function findHook(key: string): Hook | undefined {
+    return hookArr.find(value => value.key === key);
+}
+
+/**
+ * 添加 hook
+ * @param key
+ * @param hookFn {Promise<any>} 要添加的 hook
+ * @param fnName
+ * @returns {string}
+ */
+export function addHook(key: string, hookFn: HookFn<any>, fnName: string): string {
+    logger.info(`添加 hook:${key}-${fnName}`);
+    let hook = findHook(key);
+    if (!hook) {
+        hook = {
+            id: randomId(),
+            tips: fnName,
+            key: key,
+            hookFn: hookFn,
+        }
+        hookArr.push(hook);
+    }
+    return hook.id;
+}
+
+/**
+ * 移除 hook
+ * @param key
+ * @param id
+ * @returns {boolean}
+ */
+export function removeHook(key: string, id: string): boolean {
+    let hook = findHook(key);
+    if (!hook) {
+        return false;
+    }
+    let index = hookArr.findIndex(value => value.id === id);
+    if (index === -1) {
+        return false;
+    }
+    hookArr.splice(index, 1);
+    return true;
+}
+
+/**
+ * 触发 hook
+ * @param key
+ * @param args 要传递的参数
+ * @returns {Promise<any>}
+ */
+export function runHook(key: string, ...args: any[]): Promise<[any , any]> {
+    //
+    logger.info(`触发 hook:${key}`);
+    return new Promise(async (resolve, _) => {
+        let hook = findHook(key);
+        if (!hook) {
+            resolve([Error('没有找到 hook'), null]);
+            return;
+        }
+        let result = hook.hookFn(...args);
+        result.then((result) => {
+            resolve([null, result]);
+        }).catch((error) => {
+            resolve([error, null]);
+        });
+    });
+}
+
+/**
+ * 获取 hook
+ * @param key
+ * @returns {boolean|*}
+ */
+export function getHook(key: string): Hook | boolean {
+    let hook = findHook(key);
+    if (!hook) {
+        return false;
+    }
+    return hook;
+}
+
+/**
+ * 设置 hook 的 tips
+ * @param key
+ * @param tips
+ * @returns {boolean}
+ */
+export function setHookTips(key: string, tips: string): boolean {
+    let hook = findHook(key);
+    if (!hook) {
+        return false;
+    }
+    hook.tips = tips;
+    return true;
+}
+
+export default {
+    addHook,
+    removeHook,
+    runHook,
+    getHook,
+    setHookTips,
+}

+ 153 - 0
src/util/httpResult.ts

@@ -0,0 +1,153 @@
+import {Response}  from 'express';
+import Logger from "./logger.ts";
+
+const log = Logger.logger('resultHandle', 'info');
+
+
+// 定义成功响应数据类型
+interface SuccessResponse {
+    code: number;
+    data: any;
+}
+
+// 定义搜索成功响应数据类型
+interface SearchSuccessResponse {
+    code: number;
+    data: any;
+    total: number;
+    count: number;
+    page: number;
+    limit: number;
+}
+
+// 定义错误响应数据类型
+interface ErrorResponse {
+    code: number;
+    msg: string;
+}
+
+export const rCode = {
+    NotMATCH: 0,
+    OK: 1,
+    NotParam: 2,
+    NotLogin: 3,
+    NotPermission: 4,
+    CustomError: 5,
+    ServerError: 6,
+    Timeout: 7,
+    NotFound: 8,
+    ApiError: 9,
+    SaveError: 10,
+    DataRepeat: 11,
+};
+
+export function success(res: Response, data: any) {
+    const responseData: SuccessResponse = {
+        code: rCode.OK,
+        data: data
+    };
+    res.json(responseData);
+}
+
+function searchSuccess(res: Response, data: any, total: number, page: number, limit: number) {
+    const responseData: SearchSuccessResponse = {
+        code: rCode.OK,
+        data: data,
+        total: total,
+        count: data.length,
+        page: page,
+        limit: limit
+    };
+    res.json(responseData);
+}
+
+function ServerError(res: Response, code: number, msg: string) {
+    log.error(`result to server error ${msg}`);
+    const errorResponse: ErrorResponse = {
+        code: code ? code : rCode.ServerError,
+        msg: msg
+    };
+    res.json(errorResponse);
+}
+
+function paramFail(res: Response, msg: string) {
+    const errorResponse: ErrorResponse = {
+        code: rCode.NotParam,
+        msg: msg
+    };
+    res.json(errorResponse);
+}
+
+function notLogin(res: Response, msg: string) {
+    const errorResponse: ErrorResponse = {
+        code: rCode.NotLogin,
+        msg: msg
+    };
+    res.json(errorResponse);
+}
+
+export function notPermission(res: Response, msg: string) {
+    const errorResponse: ErrorResponse = {
+        code: rCode.NotPermission,
+        msg: msg
+    };
+    res.status(401).json(errorResponse);
+}
+
+function customError(res: Response, msg: string) {
+    const errorResponse: ErrorResponse = {
+        code: rCode.CustomError,
+        msg: msg
+    };
+    res.json(errorResponse);
+}
+
+function controlError(res: Response, err: any, msg: string) {
+    let errorCode, errorMsg;
+    if (msg) { errorMsg = msg; }
+    if (err) {
+        if (err.eCode) {
+            errorCode = err.eCode;
+        }
+        if (!msg && err.eMsg) {
+            errorMsg = err.eMsg;
+        } else {
+            errorMsg = err.message;
+        }
+    }
+    if (!errorCode) { errorCode = rCode.ServerError; }
+    if (!errorMsg) { errorMsg = 'server error 9999'; }
+    switch (errorCode) {
+        case rCode.OK:
+            success(res, errorMsg);
+            break;
+        case rCode.NotParam:
+            paramFail(res, errorMsg);
+            break;
+        case rCode.NotLogin:
+            notLogin(res, errorMsg);
+            break;
+        case rCode.NotPermission:
+            notPermission(res, errorMsg);
+            break;
+        case rCode.CustomError:
+            customError(res, errorMsg);
+            break;
+        case rCode.Timeout:
+        case rCode.NotFound:
+        case rCode.ApiError:
+        case rCode.SaveError:
+        case rCode.DataRepeat:
+        case rCode.ServerError:
+        default:
+            ServerError(res, errorCode, errorMsg);
+    }
+}
+
+export default {
+    success,
+    searchSuccess,
+    ServerError,
+    paramFail,
+    controlError,
+};

+ 72 - 0
src/util/logger.ts

@@ -0,0 +1,72 @@
+/*
+ * @Description: 日志工具函数
+ * @Autor: kindring
+ * @Date: 2021-12-14 14:07:17
+ * @LastEditors: kindring
+ * @LastEditTime: 2021-12-14 14:46:16
+ * @LastDescript:
+ */
+import Path from 'path';
+import log4js from 'log4js';
+import {app} from "electron";
+
+let levels: { [key: string]: log4js.Level } = {
+    'trace': log4js.levels.TRACE,
+    'debug': log4js.levels.DEBUG,
+    'info': log4js.levels.INFO,
+    'warn': log4js.levels.WARN,
+    'error': log4js.levels.ERROR,
+    'fatal': log4js.levels.FATAL,
+}
+
+const logFileName = 'fcE.log';
+const appPath = app.isPackaged ? Path.dirname(app.getPath('exe')) : app.getAppPath();
+const _path = process.env.WEBPACK_DEV_SERVER_URL?`logs/${logFileName}`: Path.resolve( appPath,`logs/${logFileName}`);
+console.log(_path);
+
+const config = {
+    // 输出到控制台的内容,同时也输出到日志文件中
+    replaceConsole: true,
+    appenders: {
+        cheese: {
+            // 设置类型为 dateFile
+            type: 'dateFile',
+            // 配置文件名
+            filename: _path,
+            // 指定编码格式为 utf-8
+            encoding: 'utf-8',
+            // 配置 layout,此处使用自定义模式 pattern
+            // layout: 'basic',
+            // 日志文件按日期(天)切割
+            pattern: "yyyy-MM-dd",
+            // 保留10天的日志文件
+            backups:10,
+            // 回滚旧的日志文件时,保证以 .log 结尾 (只有在 alwaysIncludePattern 为 false 生效)
+            keepFileExt: true,
+            // 输出的日志文件名是都始终包含 pattern 日期结尾
+            alwaysIncludePattern: true,
+        },
+        console: {
+            type: 'console'
+        }
+    },
+    categories: {
+        // 设置默认的 categories
+        default: { appenders: ['cheese', 'console'], level: 'debug' },
+    }
+}
+// 加载配置文件
+log4js.configure(config);
+
+
+function logger(name: string, level: string) {
+    const logger = log4js.getLogger(name)
+    // 默认为debug权限及以上
+    logger.level = levels[level] || levels['debug']
+    return logger
+}
+
+export default {
+    logger,
+}
+

+ 41 - 0
src/util/pageHandle.ts

@@ -0,0 +1,41 @@
+import {App} from "vue";
+
+import {ipcRenderer} from "electron";
+import {IpcAction, windowAction} from "../tools/IpcCmd.ts";
+import {registerWindowData} from "../types/appConfig.ts";
+
+function winHandle(ipc: Electron.IpcRenderer, windowName: string, action: IpcAction){
+    let sendCode = action.code;
+    ipc.send(sendCode, windowName);
+}
+
+function registerWinHandle(ipc: Electron.IpcRenderer, windowName: string): (action: IpcAction)=>void
+{
+    windowName = windowName.toString();
+    return winHandle.bind(null, ipc, windowName);
+}
+
+/**
+ * 初始化页面 挂载$winHandle为全局函数. 操作窗口
+ * @param app
+ *
+ */
+export function windowInit(app: App){
+    ipcRenderer.on(
+        windowAction.bindSignId.code,
+        (_: Electron.IpcRendererEvent , data: registerWindowData)=>
+        {
+            console.log(`获取到窗口id${data.signId}`);
+            app.config.globalProperties.$winHandle = registerWinHandle(ipcRenderer, data.signId);
+            console.log(   `窗口绑定:${windowAction.bindSignId.resCode}` );
+            ipcRenderer.send(windowAction.bindSignId.resCode, data.signId);
+        });
+    // todo 初始化axios
+    // if(axios){
+    //     axios.initAxios(
+    //         data.baseUrl,
+    //         1000,
+    //         data.key
+    //     );
+    // }
+}

+ 1 - 1
src/util/promiseHandle.ts

@@ -1,5 +1,5 @@
 // 将promise 转为
-function handle<T>(promise: Promise<T>): Promise<[Error | null | T, T | null | undefined]> {
+export function handle<T>(promise: Promise<T>): Promise<[Error | null | T, T | null | undefined]> {
     return new Promise<[Error | T | null , T | null | undefined]>(resolve => {
         promise.then(val => {
             resolve([null, val]);

+ 3 - 0
src/util/random.ts

@@ -0,0 +1,3 @@
+export function randomId() {
+    return Math.random().toString(36).substr(4);
+}

+ 5 - 0
vite.config.ts

@@ -16,4 +16,9 @@ export default defineConfig({
             plugins: [buildPlugin()],
         },
     },
+
+    server: {
+        host: '127.0.0.1',
+    },
+
 });

Some files were not shown because too many files changed in this diff