Feat: Add segmented progress bar for data extractor with dynamic phase calculation
- Add ExtractionProgress type with phase, batch, and subProgress fields - Implement dynamic progress calculation: 1 (login) + N (batches) + 2 (merge/import) - Create SegmentedProgressBar component with 4 colored phases (purple/blue/amber/green) - Show batch-level progress during download phase (e.g., 批次 1/10) - Display sub-progress during login phase (连接数据库/解析订单号/登录 ERP) - Update IPC handler and extractor services to report detailed progress - Add phase status indicators (completed/active/pending) with color-coded dots
This commit is contained in:
@@ -6,14 +6,20 @@ import { create, type IDatabaseService } from '../services/database'
|
||||
import { createLogger } from '../services/logger'
|
||||
import { withErrorHandling, type IpcResult } from './index'
|
||||
import { ErpConnectionError, ValidationError, DatabaseQueryError } from '../types/errors'
|
||||
import type { ExtractorInput, ExtractorResult } from '../types/extractor.types'
|
||||
import type { ExtractorInput, ExtractorResult, ExtractionProgress } from '../types/extractor.types'
|
||||
|
||||
const log = createLogger('ExtractorHandler')
|
||||
|
||||
function sendProgress(windowId: number, message: string, progress: number): void {
|
||||
function sendProgress(
|
||||
windowId: number,
|
||||
message: string,
|
||||
progress: number,
|
||||
extra?: Partial<ExtractionProgress>
|
||||
): void {
|
||||
try {
|
||||
const progressData = { message, progress, ...extra }
|
||||
webContents.getAllWebContents().forEach((wc) => {
|
||||
wc.send('extractor:progress', { message, progress })
|
||||
wc.send('extractor:progress', progressData)
|
||||
})
|
||||
} catch (error) {
|
||||
log.warn('Failed to send progress event', { error })
|
||||
@@ -63,7 +69,10 @@ export function registerExtractorHandlers(): void {
|
||||
|
||||
// Create database service using factory
|
||||
log.info('Connecting to database for order resolution...')
|
||||
sendProgress(windowId, '连接数据库...', 5)
|
||||
sendProgress(windowId, '连接数据库...', 3.33, {
|
||||
phase: 'login',
|
||||
subProgress: { step: '连接数据库', current: 1, total: 3 }
|
||||
})
|
||||
sendLog(windowId, 'system', '正在连接数据库...')
|
||||
|
||||
try {
|
||||
@@ -77,7 +86,10 @@ export function registerExtractorHandlers(): void {
|
||||
}
|
||||
|
||||
// Resolve order numbers (convert productionIDs to 生产订单号)
|
||||
sendProgress(windowId, '解析订单号...', 10)
|
||||
sendProgress(windowId, '解析订单号...', 6.67, {
|
||||
phase: 'login',
|
||||
subProgress: { step: '解析订单号', current: 2, total: 3 }
|
||||
})
|
||||
sendLog(windowId, 'info', '正在解析订单号...')
|
||||
|
||||
const resolver = new OrderNumberResolver(dbService)
|
||||
@@ -109,7 +121,10 @@ export function registerExtractorHandlers(): void {
|
||||
headless: true
|
||||
})
|
||||
|
||||
sendProgress(windowId, '登录 ERP 系统...', 15)
|
||||
sendProgress(windowId, '登录 ERP 系统...', 9.99, {
|
||||
phase: 'login',
|
||||
subProgress: { step: '登录 ERP 系统', current: 3, total: 3 }
|
||||
})
|
||||
sendLog(windowId, 'system', '正在登录 ERP 系统...')
|
||||
|
||||
log.info('Logging in to ERP...')
|
||||
@@ -132,8 +147,8 @@ export function registerExtractorHandlers(): void {
|
||||
const modifiedInput: ExtractorInput = {
|
||||
...input,
|
||||
orderNumbers: validOrderNumbers,
|
||||
onProgress: (message, progress) => {
|
||||
sendProgress(windowId, message, progress)
|
||||
onProgress: (message, progress, extra) => {
|
||||
sendProgress(windowId, message, progress, extra)
|
||||
sendLog(windowId, 'info', message)
|
||||
},
|
||||
onLog: (level, message) => {
|
||||
@@ -141,9 +156,6 @@ export function registerExtractorHandlers(): void {
|
||||
}
|
||||
}
|
||||
|
||||
sendProgress(windowId, '开始提取数据...', 20)
|
||||
sendLog(windowId, 'system', '提取引擎启动,开始下载数据...')
|
||||
|
||||
const result = await extractor.extract(modifiedInput)
|
||||
|
||||
// Add warnings to result errors if any
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import path from 'path'
|
||||
import { ERP_LOCATORS } from './locators'
|
||||
import type { ErpSession } from '../../types/erp.types'
|
||||
import type { ExtractorCoreInput, ExtractorCoreResult } from '../../types/extractor.types'
|
||||
import type {
|
||||
ExtractorCoreInput,
|
||||
ExtractorCoreResult,
|
||||
ExtractionProgress
|
||||
} from '../../types/extractor.types'
|
||||
|
||||
/**
|
||||
* ExtractorCore - Handles all web page operations for data extraction
|
||||
@@ -22,17 +26,25 @@ export class ExtractorCore {
|
||||
errors: []
|
||||
}
|
||||
|
||||
// Navigate to extractor page and get popup page + work frame
|
||||
const totalBatches = this.createBatches(input.orderNumbers, input.batchSize).length
|
||||
const totalPoints = 1 + totalBatches + 2
|
||||
const progressPerPoint = 100 / totalPoints
|
||||
|
||||
const { popupPage, workFrame } = await this.navigateToExtractorPage(input.session)
|
||||
|
||||
// Process orders in batches
|
||||
const batches = this.createBatches(input.orderNumbers, input.batchSize)
|
||||
|
||||
for (let i = 0; i < batches.length; i++) {
|
||||
const batch = batches[i]
|
||||
const progress = ((i + 1) / batches.length) * 100
|
||||
const progress = (1 + (i + 1)) * progressPerPoint
|
||||
|
||||
input.onProgress?.(`Processing batch ${i + 1}/${batches.length}`, progress)
|
||||
const progressExtra: Partial<ExtractionProgress> = {
|
||||
phase: 'downloading',
|
||||
currentBatch: i + 1,
|
||||
totalBatches
|
||||
}
|
||||
|
||||
input.onProgress?.(`处理批次 ${i + 1}/${totalBatches}`, progress, progressExtra)
|
||||
|
||||
try {
|
||||
const filePath = await this.downloadBatch(
|
||||
|
||||
@@ -7,7 +7,8 @@ import type {
|
||||
ExtractorInput,
|
||||
ExtractorResult,
|
||||
ImportResult,
|
||||
LogLevel
|
||||
LogLevel,
|
||||
ExtractionProgress
|
||||
} from '../../types/extractor.types'
|
||||
import { DataImportService } from '../database/data-importer'
|
||||
|
||||
@@ -64,7 +65,16 @@ export class ExtractorService {
|
||||
|
||||
// Merge downloaded files (original logic preserved)
|
||||
if (result.downloadedFiles.length > 0) {
|
||||
input.onProgress?.('正在合并文件...', 95)
|
||||
const totalBatches = result.downloadedFiles.length
|
||||
const totalPoints = 1 + totalBatches + 2
|
||||
const progressPerPoint = 100 / totalPoints
|
||||
const mergeProgress = (1 + totalBatches) * progressPerPoint
|
||||
const importProgress = (1 + totalBatches + 1) * progressPerPoint
|
||||
|
||||
input.onProgress?.('正在合并文件...', mergeProgress, {
|
||||
phase: 'merging',
|
||||
totalBatches
|
||||
})
|
||||
const mergeResult = await this.mergeFiles(result.downloadedFiles)
|
||||
result.mergedFile = mergeResult.mergedFile
|
||||
result.recordCount = mergeResult.recordCount
|
||||
@@ -79,7 +89,11 @@ export class ExtractorService {
|
||||
|
||||
// Auto-import to database if merge was successful
|
||||
if (result.mergedFile) {
|
||||
input.onProgress?.('正在写入数据库...', 98)
|
||||
const importProgress = (1 + totalBatches + 1) * progressPerPoint
|
||||
input.onProgress?.('正在写入数据库...', importProgress, {
|
||||
phase: 'importing',
|
||||
totalBatches
|
||||
})
|
||||
const importResult = await this.importToDatabaseWithLogging(
|
||||
result.mergedFile,
|
||||
input.onLog
|
||||
|
||||
@@ -2,10 +2,25 @@ import type { ErpSession } from './erp.types'
|
||||
|
||||
export type LogLevel = 'info' | 'success' | 'warning' | 'error' | 'system'
|
||||
|
||||
export type ExtractionPhase = 'login' | 'downloading' | 'merging' | 'importing'
|
||||
|
||||
export interface ExtractionProgress {
|
||||
message: string
|
||||
progress: number
|
||||
phase?: ExtractionPhase
|
||||
currentBatch?: number
|
||||
totalBatches?: number
|
||||
subProgress?: {
|
||||
step: string
|
||||
current: number
|
||||
total: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface ExtractorInput {
|
||||
orderNumbers: string[]
|
||||
batchSize?: number
|
||||
onProgress?: (message: string, progress: number) => void
|
||||
onProgress?: (message: string, progress: number, extra?: Partial<ExtractionProgress>) => void
|
||||
onLog?: (level: LogLevel, message: string) => void
|
||||
}
|
||||
|
||||
@@ -43,7 +58,7 @@ export interface ExtractorCoreInput {
|
||||
orderNumbers: string[]
|
||||
downloadDir: string
|
||||
batchSize: number
|
||||
onProgress?: (message: string, progress: number) => void
|
||||
onProgress?: (message: string, progress: number, extra?: Partial<ExtractionProgress>) => void
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* These types define the API exposed to the renderer process via contextBridge
|
||||
*/
|
||||
|
||||
import type { ExtractorInput, ExtractorResult } from './extractor.types'
|
||||
import type { ExtractorInput, ExtractorResult, ExtractionProgress } from './extractor.types'
|
||||
import type {
|
||||
CleanerInput,
|
||||
CleanerResult,
|
||||
@@ -82,7 +82,7 @@ export interface ExtractorAPI {
|
||||
* @param callback - Callback function receiving progress data
|
||||
* @returns Unsubscribe function
|
||||
*/
|
||||
onProgress: (callback: (data: { message: string; progress: number }) => void) => () => void
|
||||
onProgress: (callback: (data: ExtractionProgress) => void) => () => void
|
||||
/**
|
||||
* Subscribe to log messages
|
||||
* @param callback - Callback function receiving log data
|
||||
|
||||
Reference in New Issue
Block a user