feat: add progress indicator to cleaner execution report dialog
- Add CleanerProgress type for tracking execution progress - Implement progress calculation: (1 + i + j/Mᵢ)/(1+N) × 100 - Login complete: 1/(1+N) × 100 - Per material: (1 + orderIndex + materialIdx/totalMaterials)/(1+totalOrders) × 100 - Add cleaner.onProgress IPC event for real-time progress updates - Enhance ExecutionReportDialog with progress bar and status display - Update useCleaner hook with progress state management - Dialog opens immediately on execution start, showing progress then results
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { ipcMain } from 'electron'
|
import { ipcMain, webContents } from 'electron'
|
||||||
import { ErpAuthService } from '../services/erp/erp-auth'
|
import { ErpAuthService } from '../services/erp/erp-auth'
|
||||||
import { CleanerService } from '../services/erp/cleaner'
|
import { CleanerService } from '../services/erp/cleaner'
|
||||||
import { OrderNumberResolver } from '../services/erp/order-resolver'
|
import { OrderNumberResolver } from '../services/erp/order-resolver'
|
||||||
@@ -11,12 +11,38 @@ import { ErpConnectionError, ValidationError, DatabaseQueryError } from '../type
|
|||||||
import type {
|
import type {
|
||||||
CleanerInput,
|
CleanerInput,
|
||||||
CleanerResult,
|
CleanerResult,
|
||||||
|
CleanerProgress,
|
||||||
ExportResultItem,
|
ExportResultItem,
|
||||||
ExportResultResponse
|
ExportResultResponse
|
||||||
} from '../types/cleaner.types'
|
} from '../types/cleaner.types'
|
||||||
|
|
||||||
const log = createLogger('CleanerHandler')
|
const log = createLogger('CleanerHandler')
|
||||||
|
|
||||||
|
function sendProgress(
|
||||||
|
windowId: number,
|
||||||
|
message: string,
|
||||||
|
progress: number,
|
||||||
|
extra?: Partial<CleanerProgress>
|
||||||
|
): 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<MySqlService | SqlServerService> {
|
async function getDatabaseService(): Promise<MySqlService | SqlServerService> {
|
||||||
const dbType = process.env.DB_TYPE?.toLowerCase()
|
const dbType = process.env.DB_TYPE?.toLowerCase()
|
||||||
|
|
||||||
@@ -50,7 +76,9 @@ async function getDatabaseService(): Promise<MySqlService | SqlServerService> {
|
|||||||
export function registerCleanerHandlers(): void {
|
export function registerCleanerHandlers(): void {
|
||||||
ipcMain.handle(
|
ipcMain.handle(
|
||||||
'cleaner:run',
|
'cleaner:run',
|
||||||
async (_event, input: CleanerInput): Promise<IpcResult<CleanerResult>> => {
|
async (event, input: CleanerInput): Promise<IpcResult<CleanerResult>> => {
|
||||||
|
const windowId = event.sender.id
|
||||||
|
|
||||||
return withErrorHandling(async () => {
|
return withErrorHandling(async () => {
|
||||||
let authService: ErpAuthService | null = null
|
let authService: ErpAuthService | null = null
|
||||||
let dbService: MySqlService | SqlServerService | null = null
|
let dbService: MySqlService | SqlServerService | null = null
|
||||||
@@ -125,12 +153,25 @@ export function registerCleanerHandlers(): void {
|
|||||||
}
|
}
|
||||||
log.info('Login successful')
|
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 cleaner = new CleanerService(authService)
|
||||||
|
|
||||||
const modifiedInput: CleanerInput = {
|
const modifiedInput: CleanerInput = {
|
||||||
...input,
|
...input,
|
||||||
orderNumbers: validOrderNumbers,
|
orderNumbers: validOrderNumbers,
|
||||||
onProgress: input.onProgress
|
onProgress: (message, progress, extra) => {
|
||||||
|
sendProgress(windowId, message, progress ?? 0, extra)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info('Starting cleaning', { orderCount: validOrderNumbers.length })
|
log.info('Starting cleaning', { orderCount: validOrderNumbers.length })
|
||||||
@@ -140,6 +181,15 @@ export function registerCleanerHandlers(): void {
|
|||||||
result.errors = [...warnings, ...result.errors]
|
result.errors = [...warnings, ...result.errors]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Send completion progress
|
||||||
|
sendProgress(windowId, '清理完成', 100, {
|
||||||
|
phase: 'complete',
|
||||||
|
currentOrderIndex: totalOrders,
|
||||||
|
totalOrders,
|
||||||
|
currentMaterialIndex: 0,
|
||||||
|
totalMaterialsInOrder: 0
|
||||||
|
})
|
||||||
|
|
||||||
log.info('Cleaning completed', {
|
log.info('Cleaning completed', {
|
||||||
processedCount: result.ordersProcessed,
|
processedCount: result.ordersProcessed,
|
||||||
errorCount: result.errors.length
|
errorCount: result.errors.length
|
||||||
|
|||||||
@@ -87,6 +87,8 @@ export class CleanerService {
|
|||||||
details: []
|
details: []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const totalOrders = input.orderNumbers.length
|
||||||
|
|
||||||
// Create delete set for O(1) lookup
|
// Create delete set for O(1) lookup
|
||||||
const deleteSet = new Set(input.materialCodes)
|
const deleteSet = new Set(input.materialCodes)
|
||||||
|
|
||||||
@@ -100,14 +102,8 @@ export class CleanerService {
|
|||||||
await this.setupQueryInterface(workFrame)
|
await this.setupQueryInterface(workFrame)
|
||||||
|
|
||||||
// Process each order
|
// 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 orderNumber = input.orderNumbers[i]
|
||||||
const progress = ((i + 1) / input.orderNumbers.length) * 100
|
|
||||||
|
|
||||||
input.onProgress?.(
|
|
||||||
`Processing order ${i + 1}/${input.orderNumbers.length}: ${orderNumber}`,
|
|
||||||
progress
|
|
||||||
)
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const detail = await this.processOrder({
|
const detail = await this.processOrder({
|
||||||
@@ -115,7 +111,7 @@ export class CleanerService {
|
|||||||
popupPage,
|
popupPage,
|
||||||
orderNumber,
|
orderNumber,
|
||||||
orderIndex: i,
|
orderIndex: i,
|
||||||
totalOrders: input.orderNumbers.length,
|
totalOrders,
|
||||||
deleteSet,
|
deleteSet,
|
||||||
dryRun: input.dryRun ?? this.dryRun,
|
dryRun: input.dryRun ?? this.dryRun,
|
||||||
onProgress: input.onProgress
|
onProgress: input.onProgress
|
||||||
@@ -212,7 +208,11 @@ export class CleanerService {
|
|||||||
totalOrders: number
|
totalOrders: number
|
||||||
deleteSet: Set<string>
|
deleteSet: Set<string>
|
||||||
dryRun: boolean
|
dryRun: boolean
|
||||||
onProgress?: (message: string, progress?: number) => void
|
onProgress?: (
|
||||||
|
message: string,
|
||||||
|
progress?: number,
|
||||||
|
extra?: Partial<import('../../types/cleaner.types').CleanerProgress>
|
||||||
|
) => void
|
||||||
}): Promise<OrderCleanDetail> {
|
}): Promise<OrderCleanDetail> {
|
||||||
const {
|
const {
|
||||||
workFrame,
|
workFrame,
|
||||||
@@ -280,6 +280,19 @@ export class CleanerService {
|
|||||||
const statusMatch = statusText.replace(/\n/g, '').match(/备料状态:(.+)$/)
|
const statusMatch = statusText.replace(/\n/g, '').match(/备料状态:(.+)$/)
|
||||||
const detailStatus = statusMatch ? statusMatch[1].trim() : ''
|
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)
|
// Process based on status (Python lines 228-441)
|
||||||
if (detailStatus === '审批通过' && detailCount > 0) {
|
if (detailStatus === '审批通过' && detailCount > 0) {
|
||||||
// Click modify button (Python line 235)
|
// Click modify button (Python line 235)
|
||||||
@@ -319,10 +332,20 @@ export class CleanerService {
|
|||||||
const materialName = await this.getInputValue(childForm, /^材料名称/)
|
const materialName = await this.getInputValue(childForm, /^材料名称/)
|
||||||
const pendingQty = 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?.(
|
onProgress?.(
|
||||||
`Order ${orderNumber} - Material ${materialIdx}/${detailCount}: ${materialName}`,
|
`订单 ${orderIndex + 1}/${totalOrders} - 物料 ${materialIdx}/${detailCount}: ${materialName}`,
|
||||||
((orderIndex + materialIdx / detailCount) / totalOrders) * 100
|
progress,
|
||||||
|
{
|
||||||
|
currentOrderIndex: orderIndex + 1,
|
||||||
|
totalOrders,
|
||||||
|
currentMaterialIndex: materialIdx,
|
||||||
|
totalMaterialsInOrder: detailCount,
|
||||||
|
currentOrderNumber: orderNumber
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// Check if should delete (Python lines 284-406)
|
// Check if should delete (Python lines 284-406)
|
||||||
|
|||||||
@@ -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 {
|
export interface CleanerInput {
|
||||||
orderNumbers: string[]
|
orderNumbers: string[]
|
||||||
materialCodes: string[]
|
materialCodes: string[]
|
||||||
dryRun: boolean
|
dryRun: boolean
|
||||||
headless?: boolean
|
headless?: boolean
|
||||||
onProgress?: (message: string, progress?: number) => void
|
onProgress?: (message: string, progress?: number, extra?: Partial<CleanerProgress>) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CleanerResult {
|
export interface CleanerResult {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import type { ExtractorInput, ExtractorResult, ExtractionProgress } from './extr
|
|||||||
import type {
|
import type {
|
||||||
CleanerInput,
|
CleanerInput,
|
||||||
CleanerResult,
|
CleanerResult,
|
||||||
|
CleanerProgress,
|
||||||
ExportResultItem,
|
ExportResultItem,
|
||||||
ExportResultResponse
|
ExportResultResponse
|
||||||
} from './cleaner.types'
|
} from './cleaner.types'
|
||||||
@@ -108,6 +109,13 @@ export interface CleanerAPI {
|
|||||||
* @param items - Validation result items to export
|
* @param items - Validation result items to export
|
||||||
*/
|
*/
|
||||||
exportResults: (items: ExportResultItem[]) => Promise<ExportResultResponse>
|
exportResults: (items: ExportResultItem[]) => Promise<ExportResultResponse>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribe to progress updates
|
||||||
|
* @param callback - Callback function receiving progress data
|
||||||
|
* @returns Unsubscribe function
|
||||||
|
*/
|
||||||
|
onProgress: (callback: (data: CleanerProgress) => void) => () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { contextBridge, ipcRenderer } from 'electron'
|
|||||||
import { electronAPI } from '@electron-toolkit/preload'
|
import { electronAPI } from '@electron-toolkit/preload'
|
||||||
import type { MySqlConfig, SqlServerConfig } from '../main/types/ipc-api.types'
|
import type { MySqlConfig, SqlServerConfig } from '../main/types/ipc-api.types'
|
||||||
import type { ExtractorInput, ExtractionProgress } from '../main/types/extractor.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 { ResolverInput } from '../main/ipc/resolver-handler'
|
||||||
import type { LoginRequest } from '../main/ipc/auth-handler'
|
import type { LoginRequest } from '../main/ipc/auth-handler'
|
||||||
import type { UserInfo } from '../main/types/user.types'
|
import type { UserInfo } from '../main/types/user.types'
|
||||||
@@ -47,7 +47,14 @@ const api = {
|
|||||||
// Cleaner service
|
// Cleaner service
|
||||||
cleaner: {
|
cleaner: {
|
||||||
runCleaner: (input: CleanerInput) => ipcRenderer.invoke('cleaner:run', input),
|
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
|
// Order number resolver
|
||||||
|
|||||||
@@ -1,42 +1,72 @@
|
|||||||
/**
|
/**
|
||||||
* Execution Report Dialog - Shows execution results after cleaner runs
|
* Execution Report Dialog - Shows execution progress and results after cleaner runs
|
||||||
*
|
*
|
||||||
* Displays:
|
* Displays:
|
||||||
* - Orders processed count
|
* - Progress bar during execution
|
||||||
* - Materials deleted count
|
* - Orders processed count, Materials deleted/skipped count after completion
|
||||||
* - Materials skipped count
|
|
||||||
* - Error list (if any)
|
* - Error list (if any)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react'
|
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 {
|
interface ExecutionReportDialogProps {
|
||||||
isOpen: boolean
|
isOpen: boolean
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
ordersProcessed: number
|
ordersProcessed?: number
|
||||||
materialsDeleted: number
|
materialsDeleted?: number
|
||||||
materialsSkipped: number
|
materialsSkipped?: number
|
||||||
errors?: string[]
|
errors?: string[]
|
||||||
dryRun?: boolean
|
dryRun?: boolean
|
||||||
|
isExecuting?: boolean
|
||||||
|
progress?: CleanerProgress | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ExecutionReportDialog: React.FC<ExecutionReportDialogProps> = ({
|
export const ExecutionReportDialog: React.FC<ExecutionReportDialogProps> = ({
|
||||||
isOpen,
|
isOpen,
|
||||||
onClose,
|
onClose,
|
||||||
ordersProcessed,
|
ordersProcessed = 0,
|
||||||
materialsDeleted,
|
materialsDeleted = 0,
|
||||||
materialsSkipped,
|
materialsSkipped = 0,
|
||||||
errors = [],
|
errors = [],
|
||||||
dryRun = false
|
dryRun = false,
|
||||||
|
isExecuting = false,
|
||||||
|
progress = null
|
||||||
}) => {
|
}) => {
|
||||||
if (!isOpen) return null
|
if (!isOpen) return null
|
||||||
|
|
||||||
const hasErrors = errors.length > 0
|
const hasErrors = errors.length > 0
|
||||||
|
const showProgress = isExecuting && progress
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="execution-report-overlay" onClick={onClose}>
|
<div className="execution-report-overlay" onClick={onClose}>
|
||||||
<div className="execution-report-dialog" onClick={(e) => e.stopPropagation()}>
|
<div
|
||||||
|
className="execution-report-dialog"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
style={{ width: showProgress ? '560px' : '480px' }}
|
||||||
|
>
|
||||||
|
{showProgress ? (
|
||||||
|
// Progress View
|
||||||
|
<div className="progress-header">
|
||||||
|
<div className="progress-icon-wrapper">
|
||||||
|
<Loader2 className="progress-icon spinning" />
|
||||||
|
</div>
|
||||||
|
<h2 className="progress-title">正在执行清理...</h2>
|
||||||
|
<p className="progress-subtitle">{progress?.message || '处理中...'}</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
// Result View
|
||||||
<div className="report-header">
|
<div className="report-header">
|
||||||
<div className="report-icon-wrapper">
|
<div className="report-icon-wrapper">
|
||||||
{dryRun ? (
|
{dryRun ? (
|
||||||
@@ -58,8 +88,52 @@ export const ExecutionReportDialog: React.FC<ExecutionReportDialogProps> = ({
|
|||||||
: '所有操作已成功完成'}
|
: '所有操作已成功完成'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="report-body">
|
<div className="report-body">
|
||||||
|
{showProgress ? (
|
||||||
|
// Progress View Content
|
||||||
|
<div className="progress-content">
|
||||||
|
<div className="progress-bar-container">
|
||||||
|
<div
|
||||||
|
className="progress-bar-fill"
|
||||||
|
style={{ width: `${progress?.progress || 0}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="progress-stats">
|
||||||
|
<div className="progress-stat-item">
|
||||||
|
<span className="stat-label">订单进度</span>
|
||||||
|
<span className="stat-value">
|
||||||
|
{Math.min(progress!.currentOrderIndex, progress!.totalOrders)} /{' '}
|
||||||
|
{progress!.totalOrders}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{progress!.totalMaterialsInOrder > 0 && (
|
||||||
|
<div className="progress-stat-item">
|
||||||
|
<span className="stat-label">物料进度</span>
|
||||||
|
<span className="stat-value">
|
||||||
|
{progress!.currentMaterialIndex} / {progress!.totalMaterialsInOrder}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="progress-stat-item">
|
||||||
|
<span className="stat-label">总进度</span>
|
||||||
|
<span className="stat-value">{Math.round(progress?.progress || 0)}%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{progress?.currentOrderNumber && (
|
||||||
|
<div className="current-order-info">
|
||||||
|
<Package size={14} className="order-icon" />
|
||||||
|
<span className="order-label">当前订单:</span>
|
||||||
|
<span className="order-number">{progress.currentOrderNumber}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
// Result View Content
|
||||||
|
<>
|
||||||
<div className="stats-grid">
|
<div className="stats-grid">
|
||||||
<div className="stat-card">
|
<div className="stat-card">
|
||||||
<div className="stat-icon orders">
|
<div className="stat-icon orders">
|
||||||
@@ -131,12 +205,16 @@ export const ExecutionReportDialog: React.FC<ExecutionReportDialogProps> = ({
|
|||||||
<span>预览模式结束,数据未实际修改。确认无误后可正式执行。</span>
|
<span>预览模式结束,数据未实际修改。确认无误后可正式执行。</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="report-footer">
|
<div className="report-footer">
|
||||||
|
{!showProgress && (
|
||||||
<button className="btn-report-close" onClick={onClose}>
|
<button className="btn-report-close" onClick={onClose}>
|
||||||
关闭
|
关闭
|
||||||
</button>
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>{`
|
<style>{`
|
||||||
@@ -174,27 +252,58 @@ export const ExecutionReportDialog: React.FC<ExecutionReportDialogProps> = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.execution-report-dialog {
|
.execution-report-dialog {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
||||||
width: 480px;
|
|
||||||
max-width: 90vw;
|
max-width: 90vw;
|
||||||
animation: slideDown 0.2s ease-out;
|
animation: slideDown 0.2s ease-out;
|
||||||
|
transition: width 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.progress-header,
|
||||||
.report-header {
|
.report-header {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 24px 24px 16px 24px;
|
padding: 24px 24px 16px 24px;
|
||||||
border-bottom: 1px solid #f0f0f0;
|
border-bottom: 1px solid #f0f0f0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.progress-icon-wrapper,
|
||||||
.report-icon-wrapper {
|
.report-icon-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.progress-icon {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
color: #1890ff;
|
||||||
|
animation: spin 1.5s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-title {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-subtitle {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #666;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.report-icon {
|
.report-icon {
|
||||||
width: 48px;
|
width: 48px;
|
||||||
height: 48px;
|
height: 48px;
|
||||||
@@ -229,6 +338,112 @@ export const ExecutionReportDialog: React.FC<ExecutionReportDialogProps> = ({
|
|||||||
padding: 20px 24px;
|
padding: 20px 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Progress View Styles */
|
||||||
|
.progress-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 24px;
|
||||||
|
background: linear-gradient(90deg, #e6f7ff 0%, #bae7ff 100%);
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid #91d5ff;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar-fill {
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, #1890ff 0%, #096dd9 100%);
|
||||||
|
border-radius: 12px;
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar-fill::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
transparent 0%,
|
||||||
|
rgba(255, 255, 255, 0.3) 50%,
|
||||||
|
transparent 100%
|
||||||
|
);
|
||||||
|
animation: shimmer 1.5s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes shimmer {
|
||||||
|
0% {
|
||||||
|
transform: translateX(-100%);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateX(100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-stats {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
padding: 16px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #e8e8e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-stat-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-stat-item .stat-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-stat-item .stat-value {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1890ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-order-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 12px;
|
||||||
|
background: #f0f5ff;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid #d6e4ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-order-info .order-icon {
|
||||||
|
color: #1890ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-order-info .order-label {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-order-info .order-number {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Result View Styles */
|
||||||
.stats-grid {
|
.stats-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(3, 1fr);
|
grid-template-columns: repeat(3, 1fr);
|
||||||
@@ -368,10 +583,7 @@ export const ExecutionReportDialog: React.FC<ExecutionReportDialogProps> = ({
|
|||||||
border: 1px solid #ffd591;
|
border: 1px solid #ffd591;
|
||||||
}
|
}
|
||||||
|
|
||||||
.success-icon {
|
.success-icon,
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-icon {
|
.preview-icon {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,17 @@ export interface ValidationResult {
|
|||||||
matchedTypeKeyword?: string
|
matchedTypeKeyword?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CleanerProgress {
|
||||||
|
message: string
|
||||||
|
progress: number
|
||||||
|
currentOrderIndex: number
|
||||||
|
totalOrders: number
|
||||||
|
currentMaterialIndex: number
|
||||||
|
totalMaterialsInOrder: number
|
||||||
|
currentOrderNumber?: string
|
||||||
|
phase: 'login' | 'processing' | 'complete'
|
||||||
|
}
|
||||||
|
|
||||||
export function useCleaner() {
|
export function useCleaner() {
|
||||||
// Authentication & permissions
|
// Authentication & permissions
|
||||||
const [isAdmin, setIsAdmin] = useState(false)
|
const [isAdmin, setIsAdmin] = useState(false)
|
||||||
@@ -37,6 +48,7 @@ export function useCleaner() {
|
|||||||
const [isRunning, setIsRunning] = useState(false)
|
const [isRunning, setIsRunning] = useState(false)
|
||||||
const [isValidationRunning, setIsValidationRunning] = useState(false)
|
const [isValidationRunning, setIsValidationRunning] = useState(false)
|
||||||
const [isExporting, setIsExporting] = useState(false)
|
const [isExporting, setIsExporting] = useState(false)
|
||||||
|
const [isExecuting, setIsExecuting] = useState(false)
|
||||||
|
|
||||||
// Shared Production IDs state
|
// Shared Production IDs state
|
||||||
const [sharedProductionIdsCount, setSharedProductionIdsCount] = useState(0)
|
const [sharedProductionIdsCount, setSharedProductionIdsCount] = useState(0)
|
||||||
@@ -53,6 +65,9 @@ export function useCleaner() {
|
|||||||
errors: string[]
|
errors: string[]
|
||||||
} | null>(null)
|
} | null>(null)
|
||||||
|
|
||||||
|
// Progress state
|
||||||
|
const [progress, setProgress] = useState<CleanerProgress | null>(null)
|
||||||
|
|
||||||
// Execution settings state
|
// Execution settings state
|
||||||
const [headless, setHeadless] = useState(() => {
|
const [headless, setHeadless] = useState(() => {
|
||||||
const saved = sessionStorage.getItem('cleaner_headless')
|
const saved = sessionStorage.getItem('cleaner_headless')
|
||||||
@@ -91,6 +106,17 @@ export function useCleaner() {
|
|||||||
initializePage()
|
initializePage()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
// Subscribe to cleaner progress events
|
||||||
|
useEffect(() => {
|
||||||
|
const unsubscribeProgress = window.electron.cleaner.onProgress((data) => {
|
||||||
|
setProgress(data)
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unsubscribeProgress()
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
sessionStorage.setItem('cleaner_dryRun', dryRun.toString())
|
sessionStorage.setItem('cleaner_dryRun', dryRun.toString())
|
||||||
}, [dryRun])
|
}, [dryRun])
|
||||||
@@ -233,6 +259,10 @@ export function useCleaner() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setIsRunning(true)
|
setIsRunning(true)
|
||||||
|
setIsExecuting(true)
|
||||||
|
setProgress(null)
|
||||||
|
setIsReportDialogOpen(true)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const cleanerDataResult = await window.electron.validation.getCleanerData()
|
const cleanerDataResult = await window.electron.validation.getCleanerData()
|
||||||
if (!cleanerDataResult.success) {
|
if (!cleanerDataResult.success) {
|
||||||
@@ -261,7 +291,6 @@ export function useCleaner() {
|
|||||||
materialsSkipped: response.data.materialsSkipped,
|
materialsSkipped: response.data.materialsSkipped,
|
||||||
errors: response.data.errors
|
errors: response.data.errors
|
||||||
})
|
})
|
||||||
setIsReportDialogOpen(true)
|
|
||||||
} else {
|
} else {
|
||||||
throw new Error(response.error || '清理失败')
|
throw new Error(response.error || '清理失败')
|
||||||
}
|
}
|
||||||
@@ -269,6 +298,8 @@ export function useCleaner() {
|
|||||||
alert(err instanceof Error ? err.message : '发生未知错误')
|
alert(err instanceof Error ? err.message : '发生未知错误')
|
||||||
} finally {
|
} finally {
|
||||||
setIsRunning(false)
|
setIsRunning(false)
|
||||||
|
setIsExecuting(false)
|
||||||
|
setProgress(null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -321,6 +352,7 @@ export function useCleaner() {
|
|||||||
selectedManagers,
|
selectedManagers,
|
||||||
setSelectedManagers,
|
setSelectedManagers,
|
||||||
isRunning,
|
isRunning,
|
||||||
|
isExecuting,
|
||||||
isValidationRunning,
|
isValidationRunning,
|
||||||
isExporting,
|
isExporting,
|
||||||
sharedProductionIdsCount,
|
sharedProductionIdsCount,
|
||||||
@@ -334,6 +366,7 @@ export function useCleaner() {
|
|||||||
isReportDialogOpen,
|
isReportDialogOpen,
|
||||||
setIsReportDialogOpen,
|
setIsReportDialogOpen,
|
||||||
reportData,
|
reportData,
|
||||||
|
progress,
|
||||||
handleValidation,
|
handleValidation,
|
||||||
handleCheckboxToggle,
|
handleCheckboxToggle,
|
||||||
handleConfirmDeletion,
|
handleConfirmDeletion,
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ const CleanerPage: React.FC = () => {
|
|||||||
selectedManagers,
|
selectedManagers,
|
||||||
setSelectedManagers,
|
setSelectedManagers,
|
||||||
isRunning,
|
isRunning,
|
||||||
|
isExecuting,
|
||||||
isValidationRunning,
|
isValidationRunning,
|
||||||
isExporting,
|
isExporting,
|
||||||
isTypeDialogOpen,
|
isTypeDialogOpen,
|
||||||
@@ -47,6 +48,7 @@ const CleanerPage: React.FC = () => {
|
|||||||
isReportDialogOpen,
|
isReportDialogOpen,
|
||||||
setIsReportDialogOpen,
|
setIsReportDialogOpen,
|
||||||
reportData,
|
reportData,
|
||||||
|
progress,
|
||||||
handleValidation,
|
handleValidation,
|
||||||
handleCheckboxToggle,
|
handleCheckboxToggle,
|
||||||
handleConfirmDeletion,
|
handleConfirmDeletion,
|
||||||
@@ -419,17 +421,17 @@ const CleanerPage: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Execution Report Dialog */}
|
{/* Execution Report Dialog */}
|
||||||
{reportData && (
|
|
||||||
<ExecutionReportDialog
|
<ExecutionReportDialog
|
||||||
isOpen={isReportDialogOpen}
|
isOpen={isReportDialogOpen}
|
||||||
onClose={() => setIsReportDialogOpen(false)}
|
onClose={() => setIsReportDialogOpen(false)}
|
||||||
ordersProcessed={reportData.ordersProcessed}
|
ordersProcessed={reportData?.ordersProcessed}
|
||||||
materialsDeleted={reportData.materialsDeleted}
|
materialsDeleted={reportData?.materialsDeleted}
|
||||||
materialsSkipped={reportData.materialsSkipped}
|
materialsSkipped={reportData?.materialsSkipped}
|
||||||
errors={reportData.errors}
|
errors={reportData?.errors}
|
||||||
dryRun={dryRun}
|
dryRun={dryRun}
|
||||||
|
isExecuting={isExecuting}
|
||||||
|
progress={progress}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user