feat: python exe
This commit is contained in:
parent
6bf4ec52ec
commit
f6329ff1f7
100
electron/core/PythonManager.ts
Normal file
100
electron/core/PythonManager.ts
Normal 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;
|
|
@ -6,11 +6,15 @@ import {
|
||||||
structureMetadata,
|
structureMetadata,
|
||||||
} from "./core/dicom";
|
} from "./core/dicom";
|
||||||
import { EVENT_PARSE_DICOM } from "./ipcEvent";
|
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;
|
if (!mainWindow) return;
|
||||||
|
|
||||||
ipcMain.removeAllListeners();
|
ipcMain.removeAllListeners();
|
||||||
|
@ -20,6 +24,9 @@ const registerIpcMainHandlers = (mainWindow: Electron.BrowserWindow | null) => {
|
||||||
*/
|
*/
|
||||||
ipcMain.on("ipc-loaded", () => mainWindow.show());
|
ipcMain.on("ipc-loaded", () => mainWindow.show());
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析dicoM
|
||||||
|
*/
|
||||||
ipcMain.on(EVENT_PARSE_DICOM, async (event, file: string) => {
|
ipcMain.on(EVENT_PARSE_DICOM, async (event, file: string) => {
|
||||||
const dirDialog = await dialog.showOpenDialog(mainWindow, {
|
const dirDialog = await dialog.showOpenDialog(mainWindow, {
|
||||||
properties: ["openDirectory"],
|
properties: ["openDirectory"],
|
||||||
|
@ -34,6 +41,12 @@ const registerIpcMainHandlers = (mainWindow: Electron.BrowserWindow | null) => {
|
||||||
event.reply(EVENT_PARSE_DICOM + ":RES", result);
|
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;
|
export default registerIpcMainHandlers;
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { createRequire } from "node:module";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import registerIpcMainHandlers from "./ipcMainHandlers";
|
import registerIpcMainHandlers from "./ipcMainHandlers";
|
||||||
|
import PythonManager from "./core/PythonManager";
|
||||||
|
|
||||||
const require = createRequire(import.meta.url);
|
const require = createRequire(import.meta.url);
|
||||||
const __dirname = path.dirname(fileURLToPath(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 win: BrowserWindow | null;
|
||||||
let tray: Tray | null = null;
|
let tray: Tray | null = null;
|
||||||
|
let pythonManager: PythonManager | null;
|
||||||
const theme: "dark" | "light" = "dark";
|
const theme: "dark" | "light" = "dark";
|
||||||
|
|
||||||
const themeTitleBarStyles = {
|
const themeTitleBarStyles = {
|
||||||
|
@ -45,6 +46,7 @@ function createWindow() {
|
||||||
titleBarOverlay: { height: 36, ...themeTitleBarStyles[theme] }, // 渲染进程发消息动态改变这个
|
titleBarOverlay: { height: 36, ...themeTitleBarStyles[theme] }, // 渲染进程发消息动态改变这个
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
preload: path.join(__dirname, "preload.mjs"),
|
preload: path.join(__dirname, "preload.mjs"),
|
||||||
|
nodeIntegration: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -62,7 +64,8 @@ function createWindow() {
|
||||||
win.loadFile(path.join(RENDERER_DIST, "index.html"));
|
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() {
|
function createTray() {
|
||||||
|
@ -100,8 +103,6 @@ function createTray() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(process.platform)
|
|
||||||
|
|
||||||
function registerGlobalShortcuts() {
|
function registerGlobalShortcuts() {
|
||||||
// 注册全局快捷键 'CommandOrControl+Shift+S' 来显示应用窗口
|
// 注册全局快捷键 'CommandOrControl+Shift+S' 来显示应用窗口
|
||||||
globalShortcut.register("Option+N", () => {
|
globalShortcut.register("Option+N", () => {
|
||||||
|
@ -124,6 +125,10 @@ app.on("activate", () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.on("before-quit", () => {
|
||||||
|
pythonManager?.stopFlask();
|
||||||
|
});
|
||||||
|
|
||||||
app.whenReady().then(() => {
|
app.whenReady().then(() => {
|
||||||
createWindow();
|
createWindow();
|
||||||
createTray();
|
createTray();
|
||||||
|
|
BIN
public/flask_app
Executable file
BIN
public/flask_app
Executable file
Binary file not shown.
16
public/logo.svg
Normal file
16
public/logo.svg
Normal 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 |
10
src/App.tsx
10
src/App.tsx
|
@ -3,14 +3,17 @@ import LayoutMain from "@/pages/Layout";
|
||||||
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
|
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
|
||||||
import Aorta from "@/pages/Aorta";
|
import Aorta from "@/pages/Aorta";
|
||||||
import { Setting } from "./pages/Setting";
|
import { Setting } from "./pages/Setting";
|
||||||
|
import { Env } from "./pages/Env";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const theme = document.querySelector('html')!.getAttribute('theme') as 'dark' | 'light'
|
const theme = document.querySelector("html")!.getAttribute("theme") as
|
||||||
|
| "dark"
|
||||||
|
| "light";
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.ipcRenderer.send('ipc-loaded')
|
window.ipcRenderer.send("ipc-loaded");
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeProvider defaultTheme={theme} storageKey="vite-ui-theme">
|
<ThemeProvider defaultTheme={theme} storageKey="vite-ui-theme">
|
||||||
|
@ -18,6 +21,7 @@ function App() {
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<LayoutMain />}>
|
<Route path="/" element={<LayoutMain />}>
|
||||||
<Route index element={<Aorta />} />
|
<Route index element={<Aorta />} />
|
||||||
|
<Route path="env" element={<Env />} />
|
||||||
<Route path="setting" element={<Setting />} />
|
<Route path="setting" element={<Setting />} />
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|
76
src/components/ui/card.tsx
Normal file
76
src/components/ui/card.tsx
Normal 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 }
|
|
@ -1,7 +1,41 @@
|
||||||
import { Button } from "@/components/ui/button";
|
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 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 (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ y: 10, opacity: 0 }}
|
initial={{ y: 10, opacity: 0 }}
|
||||||
|
@ -10,7 +44,25 @@ const Aorta = () => {
|
||||||
transition={{ duration: 0.25 }}
|
transition={{ duration: 0.25 }}
|
||||||
>
|
>
|
||||||
<div className="p-4">
|
<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>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
);
|
);
|
||||||
|
|
5
src/pages/Env/index.tsx
Normal file
5
src/pages/Env/index.tsx
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import React, { useEffect } from "react";
|
||||||
|
|
||||||
|
export const Env = () => {
|
||||||
|
return <div>index</div>;
|
||||||
|
};
|
|
@ -10,12 +10,17 @@ const LayoutMain = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full">
|
<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
|
<div
|
||||||
className={`inline-flex no-drag items-center justify-between ${titleBarStyles}`}
|
className={`inline-flex no-drag items-center justify-between ${titleBarStyles}`}
|
||||||
>
|
>
|
||||||
<img src="AI.png" className="h-[24px]" />
|
<img src="logo.svg" className="h-[20px]" />
|
||||||
<MenuBar />
|
<span className="pl-2 text-xs">CVPilot 启动器</span>
|
||||||
|
{/* <MenuBar /> */}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="relative h-[calc(100%-36px)]">
|
<div className="relative h-[calc(100%-36px)]">
|
||||||
|
|
|
@ -22,7 +22,7 @@ type MenuItem = {
|
||||||
const menuItems: MenuItem[] = [
|
const menuItems: MenuItem[] = [
|
||||||
{ to: "/", name: "一键启动", icon: <IoPlayOutline size={24} /> },
|
{ to: "/", name: "一键启动", icon: <IoPlayOutline size={24} /> },
|
||||||
{ to: "/datalist", name: "数据列表", icon: <IoListOutline 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: "/models", name: "模型管理", icon: <IoCubeOutline size={24} /> },
|
||||||
{ to: "/tools", name: "小工具", icon: <IoHammerOutline size={24} /> },
|
{ to: "/tools", name: "小工具", icon: <IoHammerOutline size={24} /> },
|
||||||
{ to: "/help", name: "帮助", icon: <IoHandLeftOutline size={24} /> },
|
{ to: "/help", name: "帮助", icon: <IoHandLeftOutline size={24} /> },
|
||||||
|
|
Loading…
Reference in New Issue
Block a user