diff --git a/src/main/ipc/extractor-handler.ts b/src/main/ipc/extractor-handler.ts index 8b1e11e..37232d2 100644 --- a/src/main/ipc/extractor-handler.ts +++ b/src/main/ipc/extractor-handler.ts @@ -1,4 +1,4 @@ -import { ipcMain } from 'electron' +import { ipcMain, webContents } from 'electron' import { ErpAuthService } from '../services/erp/erp-auth' import { ExtractorService } from '../services/erp/extractor' import { OrderNumberResolver } from '../services/erp/order-resolver' @@ -10,13 +10,35 @@ import type { ExtractorInput, ExtractorResult } from '../types/extractor.types' const log = createLogger('ExtractorHandler') +function sendProgress(windowId: number, message: string, progress: number): void { + try { + webContents.getAllWebContents().forEach((wc) => { + wc.send('extractor:progress', { message, progress }) + }) + } catch (error) { + log.warn('Failed to send progress event', { error }) + } +} + +function sendLog(windowId: number, level: string, message: string): void { + try { + webContents.getAllWebContents().forEach((wc) => { + wc.send('extractor:log', { level, message }) + }) + } catch (error) { + log.warn('Failed to send log event', { error }) + } +} + /** * Register IPC handlers for extractor service */ export function registerExtractorHandlers(): void { ipcMain.handle( 'extractor:run', - async (_event, input: ExtractorInput): Promise> => { + async (event, input: ExtractorInput): Promise> => { + const windowId = event.sender.id + return withErrorHandling(async () => { let authService: ErpAuthService | null = null let dbService: IDatabaseService | null = null @@ -41,6 +63,9 @@ export function registerExtractorHandlers(): void { // Create database service using factory log.info('Connecting to database for order resolution...') + sendProgress(windowId, '连接数据库...', 5) + sendLog(windowId, 'system', '正在连接数据库...') + try { dbService = await create() } catch (error) { @@ -52,6 +77,9 @@ export function registerExtractorHandlers(): void { } // Resolve order numbers (convert productionIDs to 生产订单号) + sendProgress(windowId, '解析订单号...', 10) + sendLog(windowId, 'info', '正在解析订单号...') + const resolver = new OrderNumberResolver(dbService) const mappings = await resolver.resolve(input.orderNumbers) @@ -71,6 +99,7 @@ export function registerExtractorHandlers(): void { } log.info('Resolved order numbers', { count: validOrderNumbers.length }) + sendLog(windowId, 'info', `已解析 ${validOrderNumbers.length} 个有效订单号`) // Create auth service and login authService = new ErpAuthService({ @@ -80,6 +109,9 @@ export function registerExtractorHandlers(): void { headless: true }) + sendProgress(windowId, '登录 ERP 系统...', 15) + sendLog(windowId, 'system', '正在登录 ERP 系统...') + log.info('Logging in to ERP...') try { await authService.login() @@ -91,6 +123,7 @@ export function registerExtractorHandlers(): void { ) } log.info('Login successful') + sendLog(windowId, 'success', 'ERP 登录成功') // Create extractor service and run extraction with resolved order numbers const extractor = new ExtractorService(authService) @@ -98,9 +131,19 @@ export function registerExtractorHandlers(): void { const modifiedInput: ExtractorInput = { ...input, - orderNumbers: validOrderNumbers + orderNumbers: validOrderNumbers, + onProgress: (message, progress) => { + sendProgress(windowId, message, progress) + sendLog(windowId, 'info', message) + }, + onLog: (level, message) => { + sendLog(windowId, level, message) + } } + sendProgress(windowId, '开始提取数据...', 20) + sendLog(windowId, 'system', '提取引擎启动,开始下载数据...') + const result = await extractor.extract(modifiedInput) // Add warnings to result errors if any diff --git a/src/main/services/erp/extractor.ts b/src/main/services/erp/extractor.ts index 57d7a8d..c23225a 100644 --- a/src/main/services/erp/extractor.ts +++ b/src/main/services/erp/extractor.ts @@ -3,7 +3,12 @@ import fs from 'fs/promises' import { ExtractorCore } from './extractor-core' import { ErpAuthService } from './erp-auth' import { ExcelParser } from '../excel/excel-parser' -import type { ExtractorInput, ExtractorResult, ImportResult } from '../../types/extractor.types' +import type { + ExtractorInput, + ExtractorResult, + ImportResult, + LogLevel +} from '../../types/extractor.types' import { DataImportService } from '../database/data-importer' /** @@ -75,7 +80,10 @@ export class ExtractorService { // Auto-import to database if merge was successful if (result.mergedFile) { input.onProgress?.('正在写入数据库...', 98) - const importResult = await this.importToDatabase(result.mergedFile) + const importResult = await this.importToDatabaseWithLogging( + result.mergedFile, + input.onLog + ) result.importResult = importResult if (!importResult.success && importResult.errors.length > 0) { @@ -317,4 +325,55 @@ export class ExtractorService { } } } + + /** + * Import merged Excel data to database with logging + * @param filePath - Path to the merged Excel file + * @param onLog - Optional log callback + * @returns Import result with statistics + */ + private async importToDatabaseWithLogging( + filePath: string, + onLog?: (level: LogLevel, message: string) => void + ): Promise { + console.log(`[Extractor] Starting database import from: ${filePath}`) + onLog?.('info', `开始导入数据到数据库...`) + + const importService = new DataImportService() + + try { + const result = await importService.importFromExcel(filePath, 1000) + + console.log(`[Extractor] Import completed`, { + success: result.success, + recordsRead: result.recordsRead, + recordsDeleted: result.recordsDeleted, + recordsImported: result.recordsImported + }) + + if (result.success) { + onLog?.( + 'success', + `导入完成:读取 ${result.recordsRead} 条,删除 ${result.recordsDeleted} 条,导入 ${result.recordsImported} 条` + ) + } else if (result.errors.length > 0) { + result.errors.forEach((err) => onLog?.('error', err)) + } + + return result + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error) + console.error(`[Extractor] Import failed: ${errorMsg}`) + onLog?.('error', `导入失败:${errorMsg}`) + + return { + success: false, + recordsRead: 0, + recordsDeleted: 0, + recordsImported: 0, + uniqueSourceNumbers: 0, + errors: [errorMsg] + } + } + } } diff --git a/src/main/types/extractor.types.ts b/src/main/types/extractor.types.ts index 76c0367..6fceea0 100644 --- a/src/main/types/extractor.types.ts +++ b/src/main/types/extractor.types.ts @@ -1,9 +1,12 @@ import type { ErpSession } from './erp.types' +export type LogLevel = 'info' | 'success' | 'warning' | 'error' | 'system' + export interface ExtractorInput { orderNumbers: string[] batchSize?: number onProgress?: (message: string, progress: number) => void + onLog?: (level: LogLevel, message: string) => void } /** diff --git a/src/main/types/ipc-api.types.ts b/src/main/types/ipc-api.types.ts index 80f43a7..037901b 100644 --- a/src/main/types/ipc-api.types.ts +++ b/src/main/types/ipc-api.types.ts @@ -77,6 +77,18 @@ export interface ExtractorAPI { runExtractor: ( input: ExtractorInput ) => Promise<{ success: boolean; data?: ExtractorResult; error?: string }> + /** + * Subscribe to progress updates + * @param callback - Callback function receiving progress data + * @returns Unsubscribe function + */ + onProgress: (callback: (data: { message: string; progress: number }) => void) => () => void + /** + * Subscribe to log messages + * @param callback - Callback function receiving log data + * @returns Unsubscribe function + */ + onLog: (callback: (data: { level: string; message: string }) => void) => () => void } /** diff --git a/src/preload/index.ts b/src/preload/index.ts index 8b9c6ce..ad4f57c 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -27,7 +27,23 @@ const api = { // Extractor service extractor: { - runExtractor: (input: ExtractorInput) => ipcRenderer.invoke('extractor:run', input) + runExtractor: (input: ExtractorInput) => ipcRenderer.invoke('extractor:run', input), + onProgress: (callback: (data: { message: string; progress: number }) => void) => { + const subscription = ( + _event: Electron.IpcRendererEvent, + data: { message: string; progress: number } + ) => callback(data) + ipcRenderer.on('extractor:progress', subscription) + return () => ipcRenderer.removeListener('extractor:progress', subscription) + }, + onLog: (callback: (data: { level: string; message: string }) => void) => { + const subscription = ( + _event: Electron.IpcRendererEvent, + data: { level: string; message: string } + ) => callback(data) + ipcRenderer.on('extractor:log', subscription) + return () => ipcRenderer.removeListener('extractor:log', subscription) + } }, // Cleaner service diff --git a/src/renderer/src/components/ui/LogPanel.tsx b/src/renderer/src/components/ui/LogPanel.tsx index 51809dc..427d65c 100644 --- a/src/renderer/src/components/ui/LogPanel.tsx +++ b/src/renderer/src/components/ui/LogPanel.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useRef } from 'react' import { Terminal } from 'lucide-react' -import type { LogEntry, LogLevel, ExtractorProgress } from '../../hooks/useExtractor' +import type { LogEntry, LogLevel, ExtractorProgress } from '../../stores/extractorStore' interface LogPanelProps { logs: LogEntry[] diff --git a/src/renderer/src/hooks/useExtractor.ts b/src/renderer/src/hooks/useExtractor.ts index c123a5b..eeacad8 100644 --- a/src/renderer/src/hooks/useExtractor.ts +++ b/src/renderer/src/hooks/useExtractor.ts @@ -1,38 +1,34 @@ -import { useState, useEffect } from 'react' - -export interface ExtractorProgress { - message: string - progress: number -} - -export type LogLevel = 'info' | 'success' | 'warning' | 'error' | 'system' - -export interface LogEntry { - timestamp: string - level: LogLevel - message: string -} +import { useEffect } from 'react' +import { useExtractorStore } from '../stores/extractorStore' export function useExtractor() { - const [isRunning, setIsRunning] = useState(false) - const [progress, setProgress] = useState(null) - const [error, setError] = useState(null) - const [logs, setLogs] = useState([]) - - const addLog = (level: LogLevel, message: string) => { - const timestamp = new Date().toLocaleTimeString('zh-CN', { hour12: false }) - setLogs((prev) => [...prev, { timestamp, level, message }]) - } + const { + isRunning, + progress, + error, + logs, + setRunning, + setProgress, + setError, + addLog, + clearLogs, + resetState + } = useExtractorStore() useEffect(() => { - if (progress) { - addLog('info', progress.message) - } - }, [progress]) + const unsubscribeProgress = window.electron.extractor.onProgress((data) => { + setProgress({ message: data.message, progress: data.progress }) + }) - const clearLogs = () => { - setLogs([]) - } + const unsubscribeLog = window.electron.extractor.onLog((data) => { + addLog(data.level as any, data.message) + }) + + return () => { + unsubscribeProgress() + unsubscribeLog() + } + }, [setProgress, addLog]) const startExtraction = async (orderNumbers: string) => { if (!orderNumbers.trim()) { @@ -40,11 +36,8 @@ export function useExtractor() { return } - setIsRunning(true) - setProgress(null) - setError(null) - setLogs([]) - + resetState() + setRunning(true) addLog('system', '提取引擎启动,准备执行...') try { @@ -77,7 +70,7 @@ export function useExtractor() { setError(errMsg) addLog('error', errMsg) } finally { - setIsRunning(false) + setRunning(false) setProgress(null) } } diff --git a/src/renderer/src/stores/extractorStore.ts b/src/renderer/src/stores/extractorStore.ts new file mode 100644 index 0000000..de192aa --- /dev/null +++ b/src/renderer/src/stores/extractorStore.ts @@ -0,0 +1,59 @@ +import { create } from 'zustand' + +export type LogLevel = 'info' | 'success' | 'warning' | 'error' | 'system' + +export interface LogEntry { + timestamp: string + level: LogLevel + message: string +} + +export interface ExtractorProgress { + message: string + progress: number +} + +export interface ExtractorState { + isRunning: boolean + progress: ExtractorProgress | null + error: string | null + logs: LogEntry[] +} + +export interface ExtractorActions { + setRunning: (isRunning: boolean) => void + setProgress: (progress: ExtractorProgress | null) => void + setError: (error: string | null) => void + addLog: (level: LogLevel, message: string) => void + clearLogs: () => void + resetState: () => void +} + +const initialState: ExtractorState = { + isRunning: false, + progress: null, + error: null, + logs: [] +} + +export const useExtractorStore = create((set) => ({ + ...initialState, + + setRunning: (isRunning: boolean) => set({ isRunning }), + + setProgress: (progress: ExtractorProgress | null) => set({ progress }), + + setError: (error: string | null) => set({ error }), + + addLog: (level: LogLevel, message: string) => + set((state) => { + const timestamp = new Date().toLocaleTimeString('zh-CN', { hour12: false }) + return { + logs: [...state.logs, { timestamp, level, message }] + } + }), + + clearLogs: () => set({ logs: [] }), + + resetState: () => set(initialState) +}))