diff --git a/src/main/ipc/cleaner-handler.ts b/src/main/ipc/cleaner-handler.ts index ca056aa..10b2a3e 100644 --- a/src/main/ipc/cleaner-handler.ts +++ b/src/main/ipc/cleaner-handler.ts @@ -1,4 +1,4 @@ -import { ipcMain } from 'electron' +import { ipcMain, webContents } from 'electron' import { ErpAuthService } from '../services/erp/erp-auth' import { CleanerService } from '../services/erp/cleaner' import { OrderNumberResolver } from '../services/erp/order-resolver' @@ -11,12 +11,38 @@ import { ErpConnectionError, ValidationError, DatabaseQueryError } from '../type import type { CleanerInput, CleanerResult, + CleanerProgress, ExportResultItem, ExportResultResponse } from '../types/cleaner.types' const log = createLogger('CleanerHandler') +function sendProgress( + windowId: number, + message: string, + progress: number, + extra?: Partial +): void { + try { + const progressData: CleanerProgress = { + message, + progress, + currentOrderIndex: extra?.currentOrderIndex ?? 0, + totalOrders: extra?.totalOrders ?? 0, + currentMaterialIndex: extra?.currentMaterialIndex ?? 0, + totalMaterialsInOrder: extra?.totalMaterialsInOrder ?? 0, + currentOrderNumber: extra?.currentOrderNumber, + phase: extra?.phase ?? 'processing' + } + webContents.getAllWebContents().forEach((wc) => { + wc.send('cleaner:progress', progressData) + }) + } catch (error) { + log.warn('Failed to send progress event', { error }) + } +} + async function getDatabaseService(): Promise { const dbType = process.env.DB_TYPE?.toLowerCase() @@ -50,7 +76,9 @@ async function getDatabaseService(): Promise { export function registerCleanerHandlers(): void { ipcMain.handle( 'cleaner:run', - async (_event, input: CleanerInput): Promise> => { + async (event, input: CleanerInput): Promise> => { + const windowId = event.sender.id + return withErrorHandling(async () => { let authService: ErpAuthService | null = null let dbService: MySqlService | SqlServerService | null = null @@ -125,12 +153,25 @@ export function registerCleanerHandlers(): void { } log.info('Login successful') + // Send login complete progress + const totalOrders = validOrderNumbers.length + const loginProgress = (1 / (1 + totalOrders)) * 100 + sendProgress(windowId, 'ERP 登录成功', loginProgress, { + phase: 'login', + currentOrderIndex: 0, + totalOrders, + currentMaterialIndex: 0, + totalMaterialsInOrder: 0 + }) + const cleaner = new CleanerService(authService) const modifiedInput: CleanerInput = { ...input, orderNumbers: validOrderNumbers, - onProgress: input.onProgress + onProgress: (message, progress, extra) => { + sendProgress(windowId, message, progress ?? 0, extra) + } } log.info('Starting cleaning', { orderCount: validOrderNumbers.length }) @@ -140,6 +181,15 @@ export function registerCleanerHandlers(): void { result.errors = [...warnings, ...result.errors] } + // Send completion progress + sendProgress(windowId, '清理完成', 100, { + phase: 'complete', + currentOrderIndex: totalOrders, + totalOrders, + currentMaterialIndex: 0, + totalMaterialsInOrder: 0 + }) + log.info('Cleaning completed', { processedCount: result.ordersProcessed, errorCount: result.errors.length diff --git a/src/main/services/erp/cleaner.ts b/src/main/services/erp/cleaner.ts index 5fa93be..3267a10 100644 --- a/src/main/services/erp/cleaner.ts +++ b/src/main/services/erp/cleaner.ts @@ -87,6 +87,8 @@ export class CleanerService { details: [] } + const totalOrders = input.orderNumbers.length + // Create delete set for O(1) lookup const deleteSet = new Set(input.materialCodes) @@ -100,14 +102,8 @@ export class CleanerService { await this.setupQueryInterface(workFrame) // Process each order - for (let i = 0; i < input.orderNumbers.length; i++) { + for (let i = 0; i < totalOrders; i++) { const orderNumber = input.orderNumbers[i] - const progress = ((i + 1) / input.orderNumbers.length) * 100 - - input.onProgress?.( - `Processing order ${i + 1}/${input.orderNumbers.length}: ${orderNumber}`, - progress - ) try { const detail = await this.processOrder({ @@ -115,7 +111,7 @@ export class CleanerService { popupPage, orderNumber, orderIndex: i, - totalOrders: input.orderNumbers.length, + totalOrders, deleteSet, dryRun: input.dryRun ?? this.dryRun, onProgress: input.onProgress @@ -212,7 +208,11 @@ export class CleanerService { totalOrders: number deleteSet: Set dryRun: boolean - onProgress?: (message: string, progress?: number) => void + onProgress?: ( + message: string, + progress?: number, + extra?: Partial + ) => void }): Promise { const { workFrame, @@ -280,6 +280,19 @@ export class CleanerService { const statusMatch = statusText.replace(/\n/g, '').match(/备料状态:(.+)$/) const detailStatus = statusMatch ? statusMatch[1].trim() : '' + // Send progress for order start + onProgress?.( + `开始处理订单 ${orderIndex + 1}/${totalOrders}: ${orderNumber}`, + ((1 + orderIndex) / (1 + totalOrders)) * 100, + { + currentOrderIndex: orderIndex + 1, + totalOrders, + currentMaterialIndex: 0, + totalMaterialsInOrder: detailCount, + currentOrderNumber: orderNumber + } + ) + // Process based on status (Python lines 228-441) if (detailStatus === '审批通过' && detailCount > 0) { // Click modify button (Python line 235) @@ -319,10 +332,20 @@ export class CleanerService { const materialName = await this.getInputValue(childForm, /^材料名称/) const pendingQty = await this.getInputValue(childForm, /^累计待发数量$/) - // Report progress + // Report progress using formula: (1 + i + j/Mᵢ) / (1 + N) × 100 + // where i = orderIndex (0-based), j = materialIdx (1-based), Mᵢ = detailCount, N = totalOrders + const progress = ((1 + orderIndex + materialIdx / detailCount) / (1 + totalOrders)) * 100 + onProgress?.( - `Order ${orderNumber} - Material ${materialIdx}/${detailCount}: ${materialName}`, - ((orderIndex + materialIdx / detailCount) / totalOrders) * 100 + `订单 ${orderIndex + 1}/${totalOrders} - 物料 ${materialIdx}/${detailCount}: ${materialName}`, + progress, + { + currentOrderIndex: orderIndex + 1, + totalOrders, + currentMaterialIndex: materialIdx, + totalMaterialsInOrder: detailCount, + currentOrderNumber: orderNumber + } ) // Check if should delete (Python lines 284-406) diff --git a/src/main/types/cleaner.types.ts b/src/main/types/cleaner.types.ts index b88f732..5f487f0 100644 --- a/src/main/types/cleaner.types.ts +++ b/src/main/types/cleaner.types.ts @@ -1,9 +1,22 @@ +export type CleanerPhase = 'login' | 'processing' | 'complete' + +export interface CleanerProgress { + message: string + progress: number + currentOrderIndex: number + totalOrders: number + currentMaterialIndex: number + totalMaterialsInOrder: number + currentOrderNumber?: string + phase: CleanerPhase +} + export interface CleanerInput { orderNumbers: string[] materialCodes: string[] dryRun: boolean headless?: boolean - onProgress?: (message: string, progress?: number) => void + onProgress?: (message: string, progress?: number, extra?: Partial) => void } export interface CleanerResult { diff --git a/src/main/types/ipc-api.types.ts b/src/main/types/ipc-api.types.ts index ce9b2ca..011ebe2 100644 --- a/src/main/types/ipc-api.types.ts +++ b/src/main/types/ipc-api.types.ts @@ -7,6 +7,7 @@ import type { ExtractorInput, ExtractorResult, ExtractionProgress } from './extr import type { CleanerInput, CleanerResult, + CleanerProgress, ExportResultItem, ExportResultResponse } from './cleaner.types' @@ -108,6 +109,13 @@ export interface CleanerAPI { * @param items - Validation result items to export */ exportResults: (items: ExportResultItem[]) => Promise + + /** + * Subscribe to progress updates + * @param callback - Callback function receiving progress data + * @returns Unsubscribe function + */ + onProgress: (callback: (data: CleanerProgress) => void) => () => void } /** diff --git a/src/preload/index.ts b/src/preload/index.ts index 2718c08..56b716d 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -2,7 +2,7 @@ import { contextBridge, ipcRenderer } from 'electron' import { electronAPI } from '@electron-toolkit/preload' import type { MySqlConfig, SqlServerConfig } from '../main/types/ipc-api.types' import type { ExtractorInput, ExtractionProgress } from '../main/types/extractor.types' -import type { CleanerInput, ExportResultItem } from '../main/types/cleaner.types' +import type { CleanerInput, CleanerProgress, ExportResultItem } from '../main/types/cleaner.types' import type { ResolverInput } from '../main/ipc/resolver-handler' import type { LoginRequest } from '../main/ipc/auth-handler' import type { UserInfo } from '../main/types/user.types' @@ -47,7 +47,14 @@ const api = { // Cleaner service cleaner: { runCleaner: (input: CleanerInput) => ipcRenderer.invoke('cleaner:run', input), - exportResults: (items: ExportResultItem[]) => ipcRenderer.invoke('cleaner:exportResults', items) + exportResults: (items: ExportResultItem[]) => + ipcRenderer.invoke('cleaner:exportResults', items), + onProgress: (callback: (data: CleanerProgress) => void) => { + const subscription = (_event: Electron.IpcRendererEvent, data: CleanerProgress) => + callback(data) + ipcRenderer.on('cleaner:progress', subscription) + return () => ipcRenderer.removeListener('cleaner:progress', subscription) + } }, // Order number resolver diff --git a/src/renderer/src/components/ExecutionReportDialog.tsx b/src/renderer/src/components/ExecutionReportDialog.tsx index 7c1e22f..9a4d12a 100644 --- a/src/renderer/src/components/ExecutionReportDialog.tsx +++ b/src/renderer/src/components/ExecutionReportDialog.tsx @@ -1,142 +1,220 @@ /** - * Execution Report Dialog - Shows execution results after cleaner runs + * Execution Report Dialog - Shows execution progress and results after cleaner runs * * Displays: - * - Orders processed count - * - Materials deleted count - * - Materials skipped count + * - Progress bar during execution + * - Orders processed count, Materials deleted/skipped count after completion * - Error list (if any) */ import React from 'react' -import { CheckCircle, XCircle, SkipForward, Package } from 'lucide-react' +import { CheckCircle, XCircle, SkipForward, Package, Loader2 } from 'lucide-react' + +interface CleanerProgress { + message: string + progress: number + currentOrderIndex: number + totalOrders: number + currentMaterialIndex: number + totalMaterialsInOrder: number + currentOrderNumber?: string + phase: 'login' | 'processing' | 'complete' +} interface ExecutionReportDialogProps { isOpen: boolean onClose: () => void - ordersProcessed: number - materialsDeleted: number - materialsSkipped: number + ordersProcessed?: number + materialsDeleted?: number + materialsSkipped?: number errors?: string[] dryRun?: boolean + isExecuting?: boolean + progress?: CleanerProgress | null } export const ExecutionReportDialog: React.FC = ({ isOpen, onClose, - ordersProcessed, - materialsDeleted, - materialsSkipped, + ordersProcessed = 0, + materialsDeleted = 0, + materialsSkipped = 0, errors = [], - dryRun = false + dryRun = false, + isExecuting = false, + progress = null }) => { if (!isOpen) return null const hasErrors = errors.length > 0 + const showProgress = isExecuting && progress return (
-
e.stopPropagation()}> -
-
- {dryRun ? ( - - ) : hasErrors ? ( - - ) : ( - - )} +
e.stopPropagation()} + style={{ width: showProgress ? '560px' : '480px' }} + > + {showProgress ? ( + // Progress View +
+
+ +
+

正在执行清理...

+

{progress?.message || '处理中...'}

-

- {dryRun ? '预览执行报告' : hasErrors ? '执行完成 (有错误)' : '执行完成'} -

-

- {dryRun - ? '预览模式 - 未实际删除数据' - : hasErrors - ? '部分操作未能完成,请查看下方错误信息' - : '所有操作已成功完成'} -

-
+ ) : ( + // Result View +
+
+ {dryRun ? ( + + ) : hasErrors ? ( + + ) : ( + + )} +
+

+ {dryRun ? '预览执行报告' : hasErrors ? '执行完成 (有错误)' : '执行完成'} +

+

+ {dryRun + ? '预览模式 - 未实际删除数据' + : hasErrors + ? '部分操作未能完成,请查看下方错误信息' + : '所有操作已成功完成'} +

+
+ )}
-
-
-
- + {showProgress ? ( + // Progress View Content +
+
+
-
-
处理订单
-
{ordersProcessed}
-
-
-
-
- -
-
-
{dryRun ? '拟删除物料' : '删除物料'}
-
{materialsDeleted}
-
-
- -
-
- -
-
-
跳过物料
-
{materialsSkipped}
-
-
- - {hasErrors && ( -
-
- +
+
+ 订单进度 + + {Math.min(progress!.currentOrderIndex, progress!.totalOrders)} /{' '} + {progress!.totalOrders} +
-
-
错误数量
-
{errors.length}
-
-
- )} -
- - {hasErrors && ( -
-
错误详情
-
- {errors.map((error, index) => ( -
- - {error} + {progress!.totalMaterialsInOrder > 0 && ( +
+ 物料进度 + + {progress!.currentMaterialIndex} / {progress!.totalMaterialsInOrder} +
- ))} + )} +
+ 总进度 + {Math.round(progress?.progress || 0)}% +
-
- )} - {!hasErrors && !dryRun && ( -
- - 操作已成功完成,数据已同步到 ERP 系统 + {progress?.currentOrderNumber && ( +
+ + 当前订单: + {progress.currentOrderNumber} +
+ )}
- )} + ) : ( + // Result View Content + <> +
+
+
+ +
+
+
处理订单
+
{ordersProcessed}
+
+
- {!hasErrors && dryRun && ( -
- - 预览模式结束,数据未实际修改。确认无误后可正式执行。 -
+
+
+ +
+
+
{dryRun ? '拟删除物料' : '删除物料'}
+
{materialsDeleted}
+
+
+ +
+
+ +
+
+
跳过物料
+
{materialsSkipped}
+
+
+ + {hasErrors && ( +
+
+ +
+
+
错误数量
+
{errors.length}
+
+
+ )} +
+ + {hasErrors && ( +
+
错误详情
+
+ {errors.map((error, index) => ( +
+ + {error} +
+ ))} +
+
+ )} + + {!hasErrors && !dryRun && ( +
+ + 操作已成功完成,数据已同步到 ERP 系统 +
+ )} + + {!hasErrors && dryRun && ( +
+ + 预览模式结束,数据未实际修改。确认无误后可正式执行。 +
+ )} + )}
- + {!showProgress && ( + + )}