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:
Misaka_Company
2026-03-05 13:25:04 +08:00
parent 921ca15be6
commit dc01896d8b
8 changed files with 481 additions and 133 deletions

View File

@@ -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

View File

@@ -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)

View File

@@ -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 {

View File

@@ -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
} }
/** /**

View File

@@ -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

View File

@@ -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;
} }

View File

@@ -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,

View File

@@ -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>
) )
} }