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 { createLogger } from '../services/logger'
|
||||||
import { withErrorHandling, type IpcResult } from './index'
|
import { withErrorHandling, type IpcResult } from './index'
|
||||||
import { ErpConnectionError, ValidationError, DatabaseQueryError } from '../types/errors'
|
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')
|
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 {
|
try {
|
||||||
|
const progressData = { message, progress, ...extra }
|
||||||
webContents.getAllWebContents().forEach((wc) => {
|
webContents.getAllWebContents().forEach((wc) => {
|
||||||
wc.send('extractor:progress', { message, progress })
|
wc.send('extractor:progress', progressData)
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.warn('Failed to send progress event', { error })
|
log.warn('Failed to send progress event', { error })
|
||||||
@@ -63,7 +69,10 @@ export function registerExtractorHandlers(): void {
|
|||||||
|
|
||||||
// Create database service using factory
|
// Create database service using factory
|
||||||
log.info('Connecting to database for order resolution...')
|
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', '正在连接数据库...')
|
sendLog(windowId, 'system', '正在连接数据库...')
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -77,7 +86,10 @@ export function registerExtractorHandlers(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Resolve order numbers (convert productionIDs to 生产订单号)
|
// Resolve order numbers (convert productionIDs to 生产订单号)
|
||||||
sendProgress(windowId, '解析订单号...', 10)
|
sendProgress(windowId, '解析订单号...', 6.67, {
|
||||||
|
phase: 'login',
|
||||||
|
subProgress: { step: '解析订单号', current: 2, total: 3 }
|
||||||
|
})
|
||||||
sendLog(windowId, 'info', '正在解析订单号...')
|
sendLog(windowId, 'info', '正在解析订单号...')
|
||||||
|
|
||||||
const resolver = new OrderNumberResolver(dbService)
|
const resolver = new OrderNumberResolver(dbService)
|
||||||
@@ -109,7 +121,10 @@ export function registerExtractorHandlers(): void {
|
|||||||
headless: true
|
headless: true
|
||||||
})
|
})
|
||||||
|
|
||||||
sendProgress(windowId, '登录 ERP 系统...', 15)
|
sendProgress(windowId, '登录 ERP 系统...', 9.99, {
|
||||||
|
phase: 'login',
|
||||||
|
subProgress: { step: '登录 ERP 系统', current: 3, total: 3 }
|
||||||
|
})
|
||||||
sendLog(windowId, 'system', '正在登录 ERP 系统...')
|
sendLog(windowId, 'system', '正在登录 ERP 系统...')
|
||||||
|
|
||||||
log.info('Logging in to ERP...')
|
log.info('Logging in to ERP...')
|
||||||
@@ -132,8 +147,8 @@ export function registerExtractorHandlers(): void {
|
|||||||
const modifiedInput: ExtractorInput = {
|
const modifiedInput: ExtractorInput = {
|
||||||
...input,
|
...input,
|
||||||
orderNumbers: validOrderNumbers,
|
orderNumbers: validOrderNumbers,
|
||||||
onProgress: (message, progress) => {
|
onProgress: (message, progress, extra) => {
|
||||||
sendProgress(windowId, message, progress)
|
sendProgress(windowId, message, progress, extra)
|
||||||
sendLog(windowId, 'info', message)
|
sendLog(windowId, 'info', message)
|
||||||
},
|
},
|
||||||
onLog: (level, message) => {
|
onLog: (level, message) => {
|
||||||
@@ -141,9 +156,6 @@ export function registerExtractorHandlers(): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sendProgress(windowId, '开始提取数据...', 20)
|
|
||||||
sendLog(windowId, 'system', '提取引擎启动,开始下载数据...')
|
|
||||||
|
|
||||||
const result = await extractor.extract(modifiedInput)
|
const result = await extractor.extract(modifiedInput)
|
||||||
|
|
||||||
// Add warnings to result errors if any
|
// Add warnings to result errors if any
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { ERP_LOCATORS } from './locators'
|
import { ERP_LOCATORS } from './locators'
|
||||||
import type { ErpSession } from '../../types/erp.types'
|
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
|
* ExtractorCore - Handles all web page operations for data extraction
|
||||||
@@ -22,17 +26,25 @@ export class ExtractorCore {
|
|||||||
errors: []
|
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)
|
const { popupPage, workFrame } = await this.navigateToExtractorPage(input.session)
|
||||||
|
|
||||||
// Process orders in batches
|
|
||||||
const batches = this.createBatches(input.orderNumbers, input.batchSize)
|
const batches = this.createBatches(input.orderNumbers, input.batchSize)
|
||||||
|
|
||||||
for (let i = 0; i < batches.length; i++) {
|
for (let i = 0; i < batches.length; i++) {
|
||||||
const batch = batches[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 {
|
try {
|
||||||
const filePath = await this.downloadBatch(
|
const filePath = await this.downloadBatch(
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import type {
|
|||||||
ExtractorInput,
|
ExtractorInput,
|
||||||
ExtractorResult,
|
ExtractorResult,
|
||||||
ImportResult,
|
ImportResult,
|
||||||
LogLevel
|
LogLevel,
|
||||||
|
ExtractionProgress
|
||||||
} from '../../types/extractor.types'
|
} from '../../types/extractor.types'
|
||||||
import { DataImportService } from '../database/data-importer'
|
import { DataImportService } from '../database/data-importer'
|
||||||
|
|
||||||
@@ -64,7 +65,16 @@ export class ExtractorService {
|
|||||||
|
|
||||||
// Merge downloaded files (original logic preserved)
|
// Merge downloaded files (original logic preserved)
|
||||||
if (result.downloadedFiles.length > 0) {
|
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)
|
const mergeResult = await this.mergeFiles(result.downloadedFiles)
|
||||||
result.mergedFile = mergeResult.mergedFile
|
result.mergedFile = mergeResult.mergedFile
|
||||||
result.recordCount = mergeResult.recordCount
|
result.recordCount = mergeResult.recordCount
|
||||||
@@ -79,7 +89,11 @@ export class ExtractorService {
|
|||||||
|
|
||||||
// Auto-import to database if merge was successful
|
// Auto-import to database if merge was successful
|
||||||
if (result.mergedFile) {
|
if (result.mergedFile) {
|
||||||
input.onProgress?.('正在写入数据库...', 98)
|
const importProgress = (1 + totalBatches + 1) * progressPerPoint
|
||||||
|
input.onProgress?.('正在写入数据库...', importProgress, {
|
||||||
|
phase: 'importing',
|
||||||
|
totalBatches
|
||||||
|
})
|
||||||
const importResult = await this.importToDatabaseWithLogging(
|
const importResult = await this.importToDatabaseWithLogging(
|
||||||
result.mergedFile,
|
result.mergedFile,
|
||||||
input.onLog
|
input.onLog
|
||||||
|
|||||||
@@ -2,10 +2,25 @@ import type { ErpSession } from './erp.types'
|
|||||||
|
|
||||||
export type LogLevel = 'info' | 'success' | 'warning' | 'error' | 'system'
|
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 {
|
export interface ExtractorInput {
|
||||||
orderNumbers: string[]
|
orderNumbers: string[]
|
||||||
batchSize?: number
|
batchSize?: number
|
||||||
onProgress?: (message: string, progress: number) => void
|
onProgress?: (message: string, progress: number, extra?: Partial<ExtractionProgress>) => void
|
||||||
onLog?: (level: LogLevel, message: string) => void
|
onLog?: (level: LogLevel, message: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,7 +58,7 @@ export interface ExtractorCoreInput {
|
|||||||
orderNumbers: string[]
|
orderNumbers: string[]
|
||||||
downloadDir: string
|
downloadDir: string
|
||||||
batchSize: number
|
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
|
* 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 {
|
import type {
|
||||||
CleanerInput,
|
CleanerInput,
|
||||||
CleanerResult,
|
CleanerResult,
|
||||||
@@ -82,7 +82,7 @@ export interface ExtractorAPI {
|
|||||||
* @param callback - Callback function receiving progress data
|
* @param callback - Callback function receiving progress data
|
||||||
* @returns Unsubscribe function
|
* @returns Unsubscribe function
|
||||||
*/
|
*/
|
||||||
onProgress: (callback: (data: { message: string; progress: number }) => void) => () => void
|
onProgress: (callback: (data: ExtractionProgress) => void) => () => void
|
||||||
/**
|
/**
|
||||||
* Subscribe to log messages
|
* Subscribe to log messages
|
||||||
* @param callback - Callback function receiving log data
|
* @param callback - Callback function receiving log data
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { contextBridge, ipcRenderer } from 'electron'
|
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 } 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, 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'
|
||||||
@@ -28,11 +28,9 @@ const api = {
|
|||||||
// Extractor service
|
// Extractor service
|
||||||
extractor: {
|
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) => {
|
onProgress: (callback: (data: ExtractionProgress) => void) => {
|
||||||
const subscription = (
|
const subscription = (_event: Electron.IpcRendererEvent, data: ExtractionProgress) =>
|
||||||
_event: Electron.IpcRendererEvent,
|
callback(data)
|
||||||
data: { message: string; progress: number }
|
|
||||||
) => callback(data)
|
|
||||||
ipcRenderer.on('extractor:progress', subscription)
|
ipcRenderer.on('extractor:progress', subscription)
|
||||||
return () => ipcRenderer.removeListener('extractor:progress', subscription)
|
return () => ipcRenderer.removeListener('extractor:progress', subscription)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import React, { useEffect, useRef } from 'react'
|
import React, { useEffect, useRef } from 'react'
|
||||||
import { Terminal } from 'lucide-react'
|
import { Terminal } from 'lucide-react'
|
||||||
import type { LogEntry, LogLevel, ExtractorProgress } from '../../stores/extractorStore'
|
import type { LogEntry, LogLevel } from '../../stores/extractorStore'
|
||||||
|
import type { ExtractionProgress } from '../../stores/extractorStore'
|
||||||
|
|
||||||
interface LogPanelProps {
|
interface LogPanelProps {
|
||||||
logs: LogEntry[]
|
logs: LogEntry[]
|
||||||
progress: ExtractorProgress | null
|
progress: ExtractionProgress | null
|
||||||
onClear: () => void
|
onClear: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
181
src/renderer/src/components/ui/SegmentedProgressBar.tsx
Normal file
181
src/renderer/src/components/ui/SegmentedProgressBar.tsx
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import type { ExtractionPhase } from '../../stores/extractorStore'
|
||||||
|
|
||||||
|
interface SegmentedProgressBarProps {
|
||||||
|
progress: number
|
||||||
|
phase?: ExtractionPhase
|
||||||
|
currentBatch?: number
|
||||||
|
totalBatches?: number
|
||||||
|
subProgress?: {
|
||||||
|
step: string
|
||||||
|
current: number
|
||||||
|
total: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const PHASES: { key: ExtractionPhase; label: string; color: string }[] = [
|
||||||
|
{ key: 'login', label: '登录', color: 'bg-purple-500' },
|
||||||
|
{ key: 'downloading', label: '下载', color: 'bg-blue-500' },
|
||||||
|
{ key: 'merging', label: '合并', color: 'bg-amber-500' },
|
||||||
|
{ key: 'importing', label: '入库', color: 'bg-emerald-500' }
|
||||||
|
]
|
||||||
|
|
||||||
|
export const SegmentedProgressBar: React.FC<SegmentedProgressBarProps> = ({
|
||||||
|
progress,
|
||||||
|
phase,
|
||||||
|
currentBatch,
|
||||||
|
totalBatches,
|
||||||
|
subProgress
|
||||||
|
}) => {
|
||||||
|
// Calculate phase boundaries
|
||||||
|
const getPhaseBoundaries = () => {
|
||||||
|
if (!totalBatches) {
|
||||||
|
return { loginEnd: 10, downloadingEnd: 90, mergingEnd: 95 }
|
||||||
|
}
|
||||||
|
const totalPoints = 1 + totalBatches + 2
|
||||||
|
const progressPerPoint = 100 / totalPoints
|
||||||
|
const loginEnd = progressPerPoint
|
||||||
|
const downloadingEnd = (1 + totalBatches) * progressPerPoint
|
||||||
|
const mergingEnd = (1 + totalBatches + 1) * progressPerPoint
|
||||||
|
return { loginEnd, downloadingEnd, mergingEnd }
|
||||||
|
}
|
||||||
|
|
||||||
|
const { loginEnd, downloadingEnd, mergingEnd } = getPhaseBoundaries()
|
||||||
|
|
||||||
|
const getCurrentPhaseIndex = (): number => {
|
||||||
|
if (phase === 'downloading') return 1
|
||||||
|
if (phase === 'merging') return 2
|
||||||
|
if (phase === 'importing') return 3
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentPhaseIndex = getCurrentPhaseIndex()
|
||||||
|
|
||||||
|
const getPhaseStatus = (index: number) => {
|
||||||
|
if (index < currentPhaseIndex) return 'completed'
|
||||||
|
if (index === currentPhaseIndex) return 'active'
|
||||||
|
return 'pending'
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStatusDot = (status: string) => {
|
||||||
|
if (status === 'completed') return 'bg-emerald-600'
|
||||||
|
if (status === 'active') return 'bg-blue-600 animate-pulse'
|
||||||
|
return 'bg-slate-300'
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStatusText = (status: string) => {
|
||||||
|
if (status === 'completed') return 'text-emerald-600'
|
||||||
|
if (status === 'active') return 'text-blue-600 font-semibold'
|
||||||
|
return 'text-slate-400'
|
||||||
|
}
|
||||||
|
|
||||||
|
const getDetailText = () => {
|
||||||
|
if (phase === 'login' && subProgress) {
|
||||||
|
return `${subProgress.step} (${subProgress.current}/${subProgress.total})`
|
||||||
|
}
|
||||||
|
if (phase === 'downloading' && currentBatch !== undefined && totalBatches !== undefined) {
|
||||||
|
return `批次 ${currentBatch}/${totalBatches}`
|
||||||
|
}
|
||||||
|
if (phase === 'merging') {
|
||||||
|
return '正在合并 Excel 文件...'
|
||||||
|
}
|
||||||
|
if (phase === 'importing') {
|
||||||
|
return '正在写入数据库...'
|
||||||
|
}
|
||||||
|
return '准备中...'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate segment fills based on current phase
|
||||||
|
const getSegments = () => {
|
||||||
|
// Login phase: simple 0-10% range
|
||||||
|
if (phase === 'login') {
|
||||||
|
return [{ end: 10, fill: progress }]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unknown phase or missing data: simple progress bar
|
||||||
|
if (!phase || !totalBatches) {
|
||||||
|
return [{ end: 100, fill: progress }]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Multi-phase progress
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
end: loginEnd,
|
||||||
|
fill: Math.min(progress, loginEnd)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
end: downloadingEnd,
|
||||||
|
fill: Math.min(Math.max(progress, loginEnd), downloadingEnd)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
end: mergingEnd,
|
||||||
|
fill: Math.min(Math.max(progress, downloadingEnd), mergingEnd)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
end: 100,
|
||||||
|
fill: Math.min(Math.max(progress, mergingEnd), 100)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const segments = getSegments()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-5">
|
||||||
|
{/* 阶段标签 */}
|
||||||
|
<div className="flex justify-between mb-3">
|
||||||
|
{PHASES.map((p, index) => {
|
||||||
|
const status = getPhaseStatus(index)
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={p.key}
|
||||||
|
className={`flex items-center gap-2 ${getStatusText(status)} transition-colors`}
|
||||||
|
>
|
||||||
|
<div className={`w-3 h-3 rounded-full ${getStatusDot(status)} transition-colors`} />
|
||||||
|
<span className="text-sm">{p.label}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 分段进度条 */}
|
||||||
|
<div className="relative h-3 bg-slate-100 rounded-full overflow-hidden mb-3">
|
||||||
|
{segments.map((segment, index) => {
|
||||||
|
const prevEnd = index === 0 ? 0 : segments[index - 1].end
|
||||||
|
const segmentWidth = segment.end - prevEnd
|
||||||
|
const filledWidth = Math.max(0, segment.fill - prevEnd)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="absolute h-full"
|
||||||
|
style={{
|
||||||
|
left: `${prevEnd}%`,
|
||||||
|
width: `${segmentWidth}%`
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`h-full transition-all duration-300 ${PHASES[index].color}`}
|
||||||
|
style={{
|
||||||
|
width: `${segmentWidth > 0 ? (filledWidth / segmentWidth) * 100 : 0}%`
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{index < PHASES.length - 1 && (
|
||||||
|
<div className="absolute right-0 top-0 h-full w-px bg-white/50" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 详细信息 */}
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<div className="text-sm text-slate-600">
|
||||||
|
<span className="text-slate-500">当前进度:</span>
|
||||||
|
<span className="text-slate-800">{getDetailText()}</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-xl font-bold text-slate-800">{Math.round(progress)}%</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -17,7 +17,14 @@ export function useExtractor() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unsubscribeProgress = window.electron.extractor.onProgress((data) => {
|
const unsubscribeProgress = window.electron.extractor.onProgress((data) => {
|
||||||
setProgress({ message: data.message, progress: data.progress })
|
setProgress({
|
||||||
|
message: data.message,
|
||||||
|
progress: data.progress,
|
||||||
|
phase: data.phase,
|
||||||
|
currentBatch: data.currentBatch,
|
||||||
|
totalBatches: data.totalBatches,
|
||||||
|
subProgress: data.subProgress
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const unsubscribeLog = window.electron.extractor.onLog((data) => {
|
const unsubscribeLog = window.electron.extractor.onLog((data) => {
|
||||||
|
|||||||
@@ -3,21 +3,14 @@ import { Download, Play } from 'lucide-react'
|
|||||||
import OrderNumberInput from '../components/OrderNumberInput'
|
import OrderNumberInput from '../components/OrderNumberInput'
|
||||||
import { useExtractor } from '../hooks/useExtractor'
|
import { useExtractor } from '../hooks/useExtractor'
|
||||||
import LogPanel from '../components/ui/LogPanel'
|
import LogPanel from '../components/ui/LogPanel'
|
||||||
|
import { SegmentedProgressBar } from '../components/ui/SegmentedProgressBar'
|
||||||
|
|
||||||
const ExtractorPage: React.FC = () => {
|
const ExtractorPage: React.FC = () => {
|
||||||
const [orderNumbers, setOrderNumbers] = useState(() => {
|
const [orderNumbers, setOrderNumbers] = useState(() => {
|
||||||
return sessionStorage.getItem('extractor_orderNumbers') || ''
|
return sessionStorage.getItem('extractor_orderNumbers') || ''
|
||||||
})
|
})
|
||||||
|
|
||||||
const {
|
const { isRunning, progress, error, logs, startExtraction, clearLogs, setError } = useExtractor()
|
||||||
isRunning,
|
|
||||||
progress,
|
|
||||||
error,
|
|
||||||
logs,
|
|
||||||
startExtraction,
|
|
||||||
clearLogs,
|
|
||||||
setError
|
|
||||||
} = useExtractor()
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
sessionStorage.setItem('extractor_orderNumbers', orderNumbers)
|
sessionStorage.setItem('extractor_orderNumbers', orderNumbers)
|
||||||
@@ -82,10 +75,20 @@ const ExtractorPage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{isRunning && progress && (
|
||||||
|
<SegmentedProgressBar
|
||||||
|
progress={progress.progress}
|
||||||
|
phase={progress.phase}
|
||||||
|
currentBatch={progress.currentBatch}
|
||||||
|
totalBatches={progress.totalBatches}
|
||||||
|
subProgress={progress.subProgress}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<LogPanel logs={logs} progress={progress} onClear={clearLogs} />
|
<LogPanel logs={logs} progress={progress} onClear={clearLogs} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ExtractorPage
|
export default ExtractorPage
|
||||||
|
|||||||
@@ -2,27 +2,37 @@ import { create } from 'zustand'
|
|||||||
|
|
||||||
export type LogLevel = 'info' | 'success' | 'warning' | 'error' | 'system'
|
export type LogLevel = 'info' | 'success' | 'warning' | 'error' | 'system'
|
||||||
|
|
||||||
|
export type ExtractionPhase = 'login' | 'downloading' | 'merging' | 'importing'
|
||||||
|
|
||||||
export interface LogEntry {
|
export interface LogEntry {
|
||||||
timestamp: string
|
timestamp: string
|
||||||
level: LogLevel
|
level: LogLevel
|
||||||
message: string
|
message: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ExtractorProgress {
|
export interface ExtractionProgress {
|
||||||
message: string
|
message: string
|
||||||
progress: number
|
progress: number
|
||||||
|
phase?: ExtractionPhase
|
||||||
|
currentBatch?: number
|
||||||
|
totalBatches?: number
|
||||||
|
subProgress?: {
|
||||||
|
step: string
|
||||||
|
current: number
|
||||||
|
total: number
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ExtractorState {
|
export interface ExtractorState {
|
||||||
isRunning: boolean
|
isRunning: boolean
|
||||||
progress: ExtractorProgress | null
|
progress: ExtractionProgress | null
|
||||||
error: string | null
|
error: string | null
|
||||||
logs: LogEntry[]
|
logs: LogEntry[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ExtractorActions {
|
export interface ExtractorActions {
|
||||||
setRunning: (isRunning: boolean) => void
|
setRunning: (isRunning: boolean) => void
|
||||||
setProgress: (progress: ExtractorProgress | null) => void
|
setProgress: (progress: ExtractionProgress | null) => void
|
||||||
setError: (error: string | null) => void
|
setError: (error: string | null) => void
|
||||||
addLog: (level: LogLevel, message: string) => void
|
addLog: (level: LogLevel, message: string) => void
|
||||||
clearLogs: () => void
|
clearLogs: () => void
|
||||||
@@ -41,7 +51,7 @@ export const useExtractorStore = create<ExtractorState & ExtractorActions>((set)
|
|||||||
|
|
||||||
setRunning: (isRunning: boolean) => set({ isRunning }),
|
setRunning: (isRunning: boolean) => set({ isRunning }),
|
||||||
|
|
||||||
setProgress: (progress: ExtractorProgress | null) => set({ progress }),
|
setProgress: (progress: ExtractionProgress | null) => set({ progress }),
|
||||||
|
|
||||||
setError: (error: string | null) => set({ error }),
|
setError: (error: string | null) => set({ error }),
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user