feat: 解析多表头excel
This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
// @ts-check
|
||||
const { app, BrowserWindow, ipcMain, dialog, shell } = require('electron');
|
||||
const path = require('path');
|
||||
const { spawn } = require('child_process');
|
||||
|
||||
/** @type {import('child_process').ChildProcess | null} */
|
||||
let pythonProcess = null;
|
||||
const BACKEND_PORT = 9231;
|
||||
const BACKEND_URL = `http://127.0.0.1:${BACKEND_PORT}`;
|
||||
const isDev = !app.isPackaged;
|
||||
|
||||
function startPythonBackend() {
|
||||
const serverDir = isDev
|
||||
? path.join(__dirname, '..', '..', 'server')
|
||||
: path.join(process.resourcesPath, 'server');
|
||||
|
||||
// In dev, use Python directly; in production, use PyInstaller binary
|
||||
if (isDev) {
|
||||
const venvPython = path.join(serverDir, 'venv', 'bin', 'python3');
|
||||
const mainPy = path.join(serverDir, 'main.py');
|
||||
pythonProcess = spawn(venvPython, [mainPy], {
|
||||
cwd: serverDir,
|
||||
env: { ...process.env, ENGIMIND_PORT: String(BACKEND_PORT) },
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
});
|
||||
} else {
|
||||
const binaryName = process.platform === 'win32' ? 'engimind-server.exe' : 'engimind-server';
|
||||
const binary = path.join(serverDir, binaryName);
|
||||
pythonProcess = spawn(binary, [], {
|
||||
env: { ...process.env, ENGIMIND_PORT: String(BACKEND_PORT) },
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
});
|
||||
}
|
||||
|
||||
pythonProcess.stdout?.on('data', (data) => {
|
||||
console.log(`[server] ${data.toString().trim()}`);
|
||||
});
|
||||
pythonProcess.stderr?.on('data', (data) => {
|
||||
console.error(`[server] ${data.toString().trim()}`);
|
||||
});
|
||||
pythonProcess.on('exit', (code) => {
|
||||
console.log(`[server] exited with code ${code}`);
|
||||
pythonProcess = null;
|
||||
});
|
||||
}
|
||||
|
||||
function stopPythonBackend() {
|
||||
if (pythonProcess) {
|
||||
pythonProcess.kill('SIGTERM');
|
||||
setTimeout(() => {
|
||||
if (pythonProcess && !pythonProcess.killed) {
|
||||
pythonProcess.kill('SIGKILL');
|
||||
}
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
|
||||
async function waitForBackend(maxWait = 15000) {
|
||||
const start = Date.now();
|
||||
while (Date.now() - start < maxWait) {
|
||||
try {
|
||||
const resp = await fetch(`${BACKEND_URL}/health`);
|
||||
if (resp.ok) return true;
|
||||
} catch {}
|
||||
await new Promise(r => setTimeout(r, 300));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function createWindow() {
|
||||
const win = new BrowserWindow({
|
||||
width: 1600,
|
||||
height: 960,
|
||||
title: 'EngiMind — 工程 AI 协作空间',
|
||||
titleBarStyle: 'hiddenInset',
|
||||
backgroundColor: '#ffffff',
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, 'preload.cjs'),
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false,
|
||||
},
|
||||
});
|
||||
|
||||
if (isDev) {
|
||||
win.loadURL('http://localhost:5173');
|
||||
win.webContents.openDevTools({ mode: 'detach' });
|
||||
} else {
|
||||
win.loadFile(path.join(__dirname, '..', 'dist', 'index.html'));
|
||||
}
|
||||
|
||||
return win;
|
||||
}
|
||||
|
||||
// ── IPC Handlers ──
|
||||
|
||||
// File dialog
|
||||
ipcMain.handle('dialog:openFiles', async (_, filters) => {
|
||||
const result = await dialog.showOpenDialog({
|
||||
properties: ['openFile', 'multiSelections'],
|
||||
filters: filters || [
|
||||
{ name: '所有支持的文件', extensions: ['pdf', 'xlsx', 'xls', 'docx', 'dwg', 'dxf', 'shp', 'geojson', 'kml', 'gpkg'] },
|
||||
{ name: 'PDF', extensions: ['pdf'] },
|
||||
{ name: 'Excel', extensions: ['xlsx', 'xls'] },
|
||||
{ name: 'Word', extensions: ['docx'] },
|
||||
{ name: 'CAD', extensions: ['dwg', 'dxf'] },
|
||||
{ name: 'GIS', extensions: ['shp', 'geojson', 'kml', 'gpkg'] },
|
||||
],
|
||||
});
|
||||
return result.canceled ? [] : result.filePaths;
|
||||
});
|
||||
|
||||
ipcMain.handle('dialog:openFile', async (_, filters) => {
|
||||
const result = await dialog.showOpenDialog({
|
||||
properties: ['openFile'],
|
||||
filters: filters || [
|
||||
{ name: 'Documents', extensions: ['pdf', 'xlsx', 'xls', 'docx'] },
|
||||
],
|
||||
});
|
||||
return result.canceled ? null : result.filePaths[0];
|
||||
});
|
||||
|
||||
// Backend URL for renderer
|
||||
ipcMain.handle('getBackendURL', () => BACKEND_URL);
|
||||
|
||||
// ── App lifecycle ──
|
||||
|
||||
app.whenReady().then(async () => {
|
||||
// In dev mode, skip auto-starting Python if it's already running externally
|
||||
if (isDev) {
|
||||
// Check if backend is already running
|
||||
let alreadyRunning = false;
|
||||
try {
|
||||
const resp = await fetch(`${BACKEND_URL}/health`);
|
||||
if (resp.ok) alreadyRunning = true;
|
||||
} catch {}
|
||||
|
||||
if (!alreadyRunning) {
|
||||
startPythonBackend();
|
||||
const ready = await waitForBackend();
|
||||
if (!ready) {
|
||||
console.error('Python backend failed to start within timeout');
|
||||
}
|
||||
} else {
|
||||
console.log('[electron] Python backend already running at', BACKEND_URL);
|
||||
}
|
||||
} else {
|
||||
startPythonBackend();
|
||||
const ready = await waitForBackend();
|
||||
if (!ready) {
|
||||
console.error('Python backend failed to start within timeout');
|
||||
}
|
||||
}
|
||||
createWindow();
|
||||
});
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
stopPythonBackend();
|
||||
if (process.platform !== 'darwin') app.quit();
|
||||
});
|
||||
|
||||
app.on('activate', () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) createWindow();
|
||||
});
|
||||
|
||||
app.on('before-quit', () => {
|
||||
stopPythonBackend();
|
||||
});
|
||||
Reference in New Issue
Block a user