feat: python exe

This commit is contained in:
mozzie 2024-08-12 15:16:50 +08:00
parent 6bf4ec52ec
commit f6329ff1f7
11 changed files with 290 additions and 14 deletions

View File

@ -0,0 +1,100 @@
import http from "node:http";
import path from "node:path";
import { spawn, ChildProcess } from "node:child_process";
import { BrowserWindow } from "electron";
class PythonManager {
private flaskProcess: ChildProcess | null = null;
private intervalId: NodeJS.Timeout | null = null;
constructor(
private mainWindow: BrowserWindow | null,
private url: string,
private interval = 5000
) {}
// 启动 Python 服务
public startFlask() {
if (this.flaskProcess) {
console.log("Flask service is already running.");
return;
}
// 使用 spawn 启动 Flask 服务
this.flaskProcess = spawn(path.join(process.env.VITE_PUBLIC!, "flask_app"));
// 实时获取 stdout 日志
this.flaskProcess.stdout?.on("data", (data) => {
const message = data.toString();
console.log(`Flask stdout: ${message}`);
this.mainWindow?.webContents.send("flask", { stdout: message });
});
// 实时获取 stderr 日志
this.flaskProcess.stderr?.on("data", (data) => {
const message = data.toString();
console.error(`Flask stderr: ${message}`);
this.mainWindow?.webContents.send("flask", { stderr: message });
});
// 监听进程关闭事件
this.flaskProcess.on("close", (code) => {
console.log(`Flask process exited with code ${code}`);
this.flaskProcess = null;
this.mainWindow?.webContents.send("flask", { exited: true, code });
});
// 开始轮询服务状态
// this.startCheckingFlaskStatus();
}
// 停止 Python 服务
public stopFlask() {
if (this.flaskProcess) {
this.flaskProcess.kill();
console.log("Flask service stopped.");
this.flaskProcess = null;
}
// 停止轮询
this.stopCheckingFlaskStatus();
}
// 检查 Flask 服务状态
private checkFlaskStatus() {
if (!this.mainWindow) return;
http
.get(this.url, (res) => {
const { statusCode } = res;
this.mainWindow?.webContents.send("flask-check", {
running: statusCode === 200,
});
})
.on("error", (err) => {
console.error(`Error checking Flask service: ${err.message}`);
this.mainWindow?.webContents.send("flask-check", {
running: false,
});
});
}
// 开始轮询 Flask 服务状态
private startCheckingFlaskStatus() {
if (this.intervalId) {
console.log("Already checking Flask status.");
return;
}
this.intervalId = setInterval(() => this.checkFlaskStatus(), this.interval);
}
// 停止轮询 Flask 服务状态
private stopCheckingFlaskStatus() {
if (this.intervalId) {
clearInterval(this.intervalId);
this.intervalId = null;
}
}
}
export default PythonManager;

View File

@ -6,11 +6,15 @@ import {
structureMetadata,
} from "./core/dicom";
import { EVENT_PARSE_DICOM } from "./ipcEvent";
import PythonManager from "./core/PythonManager";
/**
*
*/
const registerIpcMainHandlers = (mainWindow: Electron.BrowserWindow | null) => {
const registerIpcMainHandlers = (
mainWindow: Electron.BrowserWindow | null,
pythonManager: PythonManager
) => {
if (!mainWindow) return;
ipcMain.removeAllListeners();
@ -20,6 +24,9 @@ const registerIpcMainHandlers = (mainWindow: Electron.BrowserWindow | null) => {
*/
ipcMain.on("ipc-loaded", () => mainWindow.show());
/**
* dicoM
*/
ipcMain.on(EVENT_PARSE_DICOM, async (event, file: string) => {
const dirDialog = await dialog.showOpenDialog(mainWindow, {
properties: ["openDirectory"],
@ -34,6 +41,12 @@ const registerIpcMainHandlers = (mainWindow: Electron.BrowserWindow | null) => {
event.reply(EVENT_PARSE_DICOM + ":RES", result);
}
});
ipcMain.on("python-service", (event, data) => {
const { running } = data;
console.log("执行调用");
running ? pythonManager.startFlask() : pythonManager.stopFlask();
});
};
export default registerIpcMainHandlers;

View File

@ -10,6 +10,7 @@ import { createRequire } from "node:module";
import { fileURLToPath } from "node:url";
import path from "node:path";
import registerIpcMainHandlers from "./ipcMainHandlers";
import PythonManager from "./core/PythonManager";
const require = createRequire(import.meta.url);
const __dirname = path.dirname(fileURLToPath(import.meta.url));
@ -26,7 +27,7 @@ process.env.VITE_PUBLIC = VITE_DEV_SERVER_URL
let win: BrowserWindow | null;
let tray: Tray | null = null;
let pythonManager: PythonManager | null;
const theme: "dark" | "light" = "dark";
const themeTitleBarStyles = {
@ -45,6 +46,7 @@ function createWindow() {
titleBarOverlay: { height: 36, ...themeTitleBarStyles[theme] }, // 渲染进程发消息动态改变这个
webPreferences: {
preload: path.join(__dirname, "preload.mjs"),
nodeIntegration: true,
},
});
@ -62,7 +64,8 @@ function createWindow() {
win.loadFile(path.join(RENDERER_DIST, "index.html"));
}
registerIpcMainHandlers(win);
pythonManager = new PythonManager(win, "http://127.0.0.1:15001", 3000);
registerIpcMainHandlers(win, pythonManager);
}
function createTray() {
@ -100,8 +103,6 @@ function createTray() {
});
}
console.log(process.platform)
function registerGlobalShortcuts() {
// 注册全局快捷键 'CommandOrControl+Shift+S' 来显示应用窗口
globalShortcut.register("Option+N", () => {
@ -124,6 +125,10 @@ app.on("activate", () => {
}
});
app.on("before-quit", () => {
pythonManager?.stopFlask();
});
app.whenReady().then(() => {
createWindow();
createTray();

BIN
public/flask_app Executable file

Binary file not shown.

16
public/logo.svg Normal file
View File

@ -0,0 +1,16 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="27"
height="27"
viewBox="0 0 45 47"
fill="none"
>
<path
d="M24.273 7.7653L19.6087 11.3716L15.3793 5.98863L11 7L30 35.5L34.1633 30.0428L32.7808 28.1213C32.1142 27.1213 31.6711 25.3464 33.2711 24.1464C34.8711 22.9464 36.6061 23.6397 37.4394 24.473L40.7354 28.7305L30 47L0 0H18.0882L24.273 7.7653Z"
fill="rgb(3, 148, 130)"
></path>
<path
d="M43.645 9.14625C42.6195 7.84489 40.7332 7.62124 39.4319 8.64673L30.4992 15.6858L28.646 13.3226C27.6235 12.0188 25.7378 11.7907 24.434 12.8132C23.1302 13.8356 22.9022 15.7213 23.9246 17.0251L29.4784 24.1071L29.4954 24.0939L29.506 24.1074L43.1455 13.3594C44.4468 12.3339 44.6705 10.4476 43.645 9.14625Z"
fill="rgb(3, 148, 130)"
></path>
</svg>

After

Width:  |  Height:  |  Size: 850 B

View File

@ -3,14 +3,17 @@ import LayoutMain from "@/pages/Layout";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import Aorta from "@/pages/Aorta";
import { Setting } from "./pages/Setting";
import { Env } from "./pages/Env";
import { useEffect } from "react";
function App() {
const theme = document.querySelector('html')!.getAttribute('theme') as 'dark' | 'light'
const theme = document.querySelector("html")!.getAttribute("theme") as
| "dark"
| "light";
useEffect(() => {
window.ipcRenderer.send('ipc-loaded')
}, [])
window.ipcRenderer.send("ipc-loaded");
}, []);
return (
<ThemeProvider defaultTheme={theme} storageKey="vite-ui-theme">
@ -18,6 +21,7 @@ function App() {
<Routes>
<Route path="/" element={<LayoutMain />}>
<Route index element={<Aorta />} />
<Route path="env" element={<Env />} />
<Route path="setting" element={<Setting />} />
</Route>
</Routes>

View File

@ -0,0 +1,76 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-xl border bg-card text-card-foreground shadow",
className
)}
{...props}
/>
))
Card.displayName = "Card"
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
))
CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn("font-semibold leading-none tracking-tight", className)}
{...props}
/>
))
CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
CardDescription.displayName = "CardDescription"
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
))
CardFooter.displayName = "CardFooter"
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }

View File

@ -1,7 +1,41 @@
import { Button } from "@/components/ui/button";
import { motion } from 'framer-motion';
import { motion } from "framer-motion";
import { useEffect, useState } from "react";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { IoCheckmarkCircleSharp } from "react-icons/io5";
const Aorta = () => {
const [flaskWaiting, setFlaskWaiting] = useState(false);
const [flaskRunning, setflaskRunning] = useState(false);
const [flaskInfo, setFlaskInfo] = useState("");
const handleBootPythonServer = () => {
if (!flaskRunning) setFlaskWaiting(true);
window.ipcRenderer.send("python-service", { running: !flaskRunning });
};
useEffect(() => {
window.ipcRenderer.on("flask", (event, data) => {
console.log(data);
if (data.stdout || data.stderr) {
setflaskRunning(true);
setFlaskWaiting(false);
setFlaskInfo((p) => p + (data.stdout || data.stderr));
}
if (data.exited) {
setFlaskInfo("");
setflaskRunning(false);
}
});
}, []);
return (
<motion.div
initial={{ y: 10, opacity: 0 }}
@ -10,7 +44,25 @@ const Aorta = () => {
transition={{ duration: 0.25 }}
>
<div className="p-4">
<div className="w-full h-[250px] rounded-md" style={{ backgroundImage: `url('/banner.png')`, backgroundSize: 'cover' }}></div>
<div
className="w-full h-[250px] rounded-md"
style={{
backgroundImage: `url('/banner.png')`,
backgroundSize: "cover",
}}
></div>
<div className="mt-4">
<pre>{flaskInfo}</pre>
</div>
<Button
disabled={flaskWaiting}
className="bg"
size={"lg"}
onClick={handleBootPythonServer}
>
<IoCheckmarkCircleSharp className={`mr-2 h-4 w-4}`} />
{flaskRunning ? "运行中" : "启动"} python
</Button>
</div>
</motion.div>
);

5
src/pages/Env/index.tsx Normal file
View File

@ -0,0 +1,5 @@
import React, { useEffect } from "react";
export const Env = () => {
return <div>index</div>;
};

View File

@ -10,12 +10,17 @@ const LayoutMain = () => {
return (
<div className="h-full">
<div className={`title-bar drag h-[36px] flex`}>
<div
className={`title-bar drag h-[36px] flex ${
platform === "macos" ? "justify-center" : ""
}`}
>
<div
className={`inline-flex no-drag items-center justify-between ${titleBarStyles}`}
>
<img src="AI.png" className="h-[24px]" />
<MenuBar />
<img src="logo.svg" className="h-[20px]" />
<span className="pl-2 text-xs">CVPilot </span>
{/* <MenuBar /> */}
</div>
</div>
<div className="relative h-[calc(100%-36px)]">

View File

@ -22,7 +22,7 @@ type MenuItem = {
const menuItems: MenuItem[] = [
{ to: "/", name: "一键启动", icon: <IoPlayOutline size={24} /> },
{ to: "/datalist", name: "数据列表", icon: <IoListOutline size={24} /> },
{ to: "/version", name: "版本管理", icon: <IoLayersOutline size={24} /> },
{ to: "/env", name: "环境管理", icon: <IoLayersOutline size={24} /> },
{ to: "/models", name: "模型管理", icon: <IoCubeOutline size={24} /> },
{ to: "/tools", name: "小工具", icon: <IoHammerOutline size={24} /> },
{ to: "/help", name: "帮助", icon: <IoHandLeftOutline size={24} /> },