✨ feat(cleaner): add automatic retry mechanism for failed orders
- Add retry logic with max 2 attempts per failed order - Track retry statistics (retriedOrders, successfulRetries) - Generate detailed retry report section in execution reports - Display retry metrics in ExecutionReportDialog UI Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -296,7 +296,8 @@ export function registerValidationHandlers(): void {
|
|||||||
if (sourceNumbers.length === 0) {
|
if (sourceNumbers.length === 0) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: '共享的 Production ID 没有找到对应的订单数据。请确保在数据提取页面输入了有效的 Production ID 并成功获取了订单数据。',
|
error:
|
||||||
|
'共享的 Production ID 没有找到对应的订单数据。请确保在数据提取页面输入了有效的 Production ID 并成功获取了订单数据。',
|
||||||
stats: {
|
stats: {
|
||||||
totalRecords: 0,
|
totalRecords: 0,
|
||||||
matchedCount: 0,
|
matchedCount: 0,
|
||||||
@@ -315,7 +316,8 @@ export function registerValidationHandlers(): void {
|
|||||||
if (sourceNumbers.length === 0) {
|
if (sourceNumbers.length === 0) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: '文件中的 Production ID 没有找到对应的订单数据。请检查 Production ID 是否正确,或确保数据库中有对应的订单数据。',
|
error:
|
||||||
|
'文件中的 Production ID 没有找到对应的订单数据。请检查 Production ID 是否正确,或确保数据库中有对应的订单数据。',
|
||||||
stats: {
|
stats: {
|
||||||
totalRecords: 0,
|
totalRecords: 0,
|
||||||
matchedCount: 0,
|
matchedCount: 0,
|
||||||
|
|||||||
@@ -7,6 +7,12 @@ import { createLogger } from '../logger'
|
|||||||
|
|
||||||
const log = createLogger('CleanerService')
|
const log = createLogger('CleanerService')
|
||||||
|
|
||||||
|
interface RetryResult {
|
||||||
|
retriedOrders: number
|
||||||
|
successfulRetries: number
|
||||||
|
updatedDetails: OrderCleanDetail[]
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cleaner Service Options
|
* Cleaner Service Options
|
||||||
*/
|
*/
|
||||||
@@ -102,7 +108,9 @@ export class CleanerService {
|
|||||||
materialsDeleted: 0,
|
materialsDeleted: 0,
|
||||||
materialsSkipped: 0,
|
materialsSkipped: 0,
|
||||||
errors: [],
|
errors: [],
|
||||||
details: []
|
details: [],
|
||||||
|
retriedOrders: 0,
|
||||||
|
successfulRetries: 0
|
||||||
}
|
}
|
||||||
|
|
||||||
const totalOrders = input.orderNumbers.length
|
const totalOrders = input.orderNumbers.length
|
||||||
@@ -158,7 +166,11 @@ export class CleanerService {
|
|||||||
materialsDeleted: 0,
|
materialsDeleted: 0,
|
||||||
materialsSkipped: 0,
|
materialsSkipped: 0,
|
||||||
errors: [message],
|
errors: [message],
|
||||||
skippedMaterials: []
|
skippedMaterials: [],
|
||||||
|
retryCount: 0,
|
||||||
|
retryAttempts: [],
|
||||||
|
retriedAt: undefined,
|
||||||
|
retrySuccess: false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -171,6 +183,36 @@ export class CleanerService {
|
|||||||
materialsSkipped: result.materialsSkipped,
|
materialsSkipped: result.materialsSkipped,
|
||||||
errorCount: result.errors.length
|
errorCount: result.errors.length
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Retry failed orders
|
||||||
|
const retryResult = await this.retryFailedOrders({
|
||||||
|
workFrame,
|
||||||
|
popupPage,
|
||||||
|
failedDetails: result.details.filter((d) => d.errors.length > 0),
|
||||||
|
deleteSet,
|
||||||
|
dryRun,
|
||||||
|
onProgress: input.onProgress
|
||||||
|
})
|
||||||
|
|
||||||
|
// Merge retry results
|
||||||
|
result.retriedOrders = retryResult.retriedOrders
|
||||||
|
result.successfulRetries = retryResult.successfulRetries
|
||||||
|
|
||||||
|
// Update details with retry information
|
||||||
|
retryResult.updatedDetails.forEach((updatedDetail) => {
|
||||||
|
const index = result.details.findIndex((d) => d.orderNumber === updatedDetail.orderNumber)
|
||||||
|
if (index !== -1) {
|
||||||
|
result.details[index] = updatedDetail
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Clear errors for successfully retried orders
|
||||||
|
const successfulRetryOrders = new Set(
|
||||||
|
retryResult.updatedDetails.filter((d) => d.retrySuccess).map((d) => d.orderNumber)
|
||||||
|
)
|
||||||
|
result.errors = result.errors.filter(
|
||||||
|
(err) => !successfulRetryOrders.has(err.split(':')[0].replace('Order ', ''))
|
||||||
|
)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof Error ? error.message : 'Unknown error'
|
const message = error instanceof Error ? error.message : 'Unknown error'
|
||||||
log.error('Cleaner failed', { error: message })
|
log.error('Cleaner failed', { error: message })
|
||||||
@@ -265,7 +307,11 @@ export class CleanerService {
|
|||||||
materialsDeleted: 0,
|
materialsDeleted: 0,
|
||||||
materialsSkipped: 0,
|
materialsSkipped: 0,
|
||||||
errors: [],
|
errors: [],
|
||||||
skippedMaterials: []
|
skippedMaterials: [],
|
||||||
|
retryCount: 0,
|
||||||
|
retryAttempts: [],
|
||||||
|
retriedAt: undefined,
|
||||||
|
retrySuccess: false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Query the order (Python lines 187-189)
|
// Query the order (Python lines 187-189)
|
||||||
@@ -531,4 +577,121 @@ export class CleanerService {
|
|||||||
private delay(ms: number): Promise<void> {
|
private delay(ms: number): Promise<void> {
|
||||||
return new Promise((resolve) => setTimeout(resolve, ms))
|
return new Promise((resolve) => setTimeout(resolve, ms))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retry failed orders from the initial execution
|
||||||
|
* Maximum 2 retry attempts per failed order
|
||||||
|
*/
|
||||||
|
private async retryFailedOrders(params: {
|
||||||
|
workFrame: FrameLocator
|
||||||
|
popupPage: Page
|
||||||
|
failedDetails: OrderCleanDetail[]
|
||||||
|
deleteSet: Set<string>
|
||||||
|
dryRun: boolean
|
||||||
|
onProgress?: (
|
||||||
|
message: string,
|
||||||
|
progress?: number,
|
||||||
|
extra?: Partial<import('../../types/cleaner.types').CleanerProgress>
|
||||||
|
) => void
|
||||||
|
}): Promise<RetryResult> {
|
||||||
|
const { workFrame, popupPage, failedDetails, deleteSet, dryRun, onProgress } = params
|
||||||
|
|
||||||
|
const result: RetryResult = {
|
||||||
|
retriedOrders: 0,
|
||||||
|
successfulRetries: 0,
|
||||||
|
updatedDetails: []
|
||||||
|
}
|
||||||
|
|
||||||
|
if (failedDetails.length === 0) {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info('Starting retry for failed orders', { count: failedDetails.length })
|
||||||
|
|
||||||
|
const MAX_RETRIES = 2
|
||||||
|
|
||||||
|
for (const failedDetail of failedDetails) {
|
||||||
|
const orderNumber = failedDetail.orderNumber
|
||||||
|
const retryAttempts: import('../../types/cleaner.types').RetryAttempt[] = []
|
||||||
|
|
||||||
|
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
||||||
|
try {
|
||||||
|
log.info(`Retrying order ${orderNumber} (attempt ${attempt}/${MAX_RETRIES})`)
|
||||||
|
|
||||||
|
// Create a new detail for retry
|
||||||
|
const retryDetail: OrderCleanDetail = {
|
||||||
|
orderNumber,
|
||||||
|
materialsDeleted: 0,
|
||||||
|
materialsSkipped: 0,
|
||||||
|
errors: [],
|
||||||
|
skippedMaterials: [],
|
||||||
|
retryCount: attempt,
|
||||||
|
retryAttempts: [],
|
||||||
|
retriedAt: undefined,
|
||||||
|
retrySuccess: false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-run the order processing
|
||||||
|
await this.processOrder({
|
||||||
|
workFrame,
|
||||||
|
popupPage,
|
||||||
|
orderNumber,
|
||||||
|
orderIndex: 0, // Not used for retry
|
||||||
|
totalOrders: failedDetails.length,
|
||||||
|
deleteSet,
|
||||||
|
dryRun,
|
||||||
|
onProgress: (message, progress, extra) => {
|
||||||
|
onProgress?.(
|
||||||
|
`[重试 ${attempt}/${MAX_RETRIES}] ${message}`,
|
||||||
|
progress,
|
||||||
|
extra ? { ...extra, phase: 'processing' as const } : undefined
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// If we reach here, retry succeeded
|
||||||
|
result.successfulRetries++
|
||||||
|
log.info(`Retry succeeded for order ${orderNumber}`)
|
||||||
|
|
||||||
|
// Merge the successful retry detail
|
||||||
|
result.updatedDetails.push({
|
||||||
|
...retryDetail,
|
||||||
|
retriedAt: Date.now(),
|
||||||
|
retrySuccess: true
|
||||||
|
})
|
||||||
|
result.retriedOrders++
|
||||||
|
break // Exit retry loop for this order
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : 'Unknown error'
|
||||||
|
log.warn(`Retry attempt ${attempt} failed for order ${orderNumber}: ${message}`)
|
||||||
|
|
||||||
|
retryAttempts.push({
|
||||||
|
attempt,
|
||||||
|
error: message,
|
||||||
|
timestamp: Date.now()
|
||||||
|
})
|
||||||
|
|
||||||
|
if (attempt === MAX_RETRIES) {
|
||||||
|
// All retries exhausted
|
||||||
|
log.error(`All retries failed for order ${orderNumber}`)
|
||||||
|
result.updatedDetails.push({
|
||||||
|
...failedDetail,
|
||||||
|
retryCount: MAX_RETRIES,
|
||||||
|
retryAttempts,
|
||||||
|
retriedAt: Date.now(),
|
||||||
|
retrySuccess: false
|
||||||
|
})
|
||||||
|
result.retriedOrders++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info('Retry process completed', {
|
||||||
|
retriedOrders: result.retriedOrders,
|
||||||
|
successfulRetries: result.successfulRetries
|
||||||
|
})
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,6 +89,10 @@ export class CleanerReportGenerator {
|
|||||||
lines.push(`| **删除物料数** | \`${result.materialsDeleted}\``)
|
lines.push(`| **删除物料数** | \`${result.materialsDeleted}\``)
|
||||||
lines.push(`| **跳过物料数** | \`${result.materialsSkipped}\``)
|
lines.push(`| **跳过物料数** | \`${result.materialsSkipped}\``)
|
||||||
lines.push(`| **错误数量** | \`${result.errors.length}\``)
|
lines.push(`| **错误数量** | \`${result.errors.length}\``)
|
||||||
|
if (result.retriedOrders > 0) {
|
||||||
|
lines.push(`| **重试订单数** | \`${result.retriedOrders}\``)
|
||||||
|
lines.push(`| **成功重试数** | \`${result.successfulRetries}\``)
|
||||||
|
}
|
||||||
lines.push(`| **执行耗时** | \`${this.formatDuration(options.startTime, options.endTime)}\``)
|
lines.push(`| **执行耗时** | \`${this.formatDuration(options.startTime, options.endTime)}\``)
|
||||||
lines.push('')
|
lines.push('')
|
||||||
lines.push('---')
|
lines.push('---')
|
||||||
@@ -100,6 +104,12 @@ export class CleanerReportGenerator {
|
|||||||
lines.push('| ----------- | ---- | ------ |')
|
lines.push('| ----------- | ---- | ------ |')
|
||||||
lines.push(`| ✅ 成功订单 | ${stats.successCount} | ${stats.successRate.toFixed(1)}% |`)
|
lines.push(`| ✅ 成功订单 | ${stats.successCount} | ${stats.successRate.toFixed(1)}% |`)
|
||||||
lines.push(`| ❌ 失败订单 | ${stats.failureCount} | ${(100 - stats.successRate).toFixed(1)}% |`)
|
lines.push(`| ❌ 失败订单 | ${stats.failureCount} | ${(100 - stats.successRate).toFixed(1)}% |`)
|
||||||
|
if (result.retriedOrders > 0) {
|
||||||
|
const retrySuccessRate =
|
||||||
|
result.retriedOrders > 0 ? (result.successfulRetries / result.retriedOrders) * 100 : 0
|
||||||
|
lines.push(`| 🔄 重试订单 | ${result.retriedOrders} | 100% |`)
|
||||||
|
lines.push(`| ✅ 成功重试 | ${result.successfulRetries} | ${retrySuccessRate.toFixed(1)}% |`)
|
||||||
|
}
|
||||||
lines.push('')
|
lines.push('')
|
||||||
lines.push('---')
|
lines.push('---')
|
||||||
lines.push('')
|
lines.push('')
|
||||||
@@ -111,10 +121,19 @@ export class CleanerReportGenerator {
|
|||||||
|
|
||||||
result.details.forEach((detail, index) => {
|
result.details.forEach((detail, index) => {
|
||||||
const orderNum = index + 1
|
const orderNum = index + 1
|
||||||
const status = detail.errors.length > 0 ? '❌ 失败' : '✅ 成功'
|
let status = detail.errors.length > 0 ? '❌ 失败' : '✅ 成功'
|
||||||
|
|
||||||
|
// Override status if retry was successful
|
||||||
|
if (detail.retrySuccess) {
|
||||||
|
status = '✅ 重试成功'
|
||||||
|
} else if (detail.retryCount > 0 && !detail.retrySuccess) {
|
||||||
|
status = '❌ 重试失败'
|
||||||
|
}
|
||||||
|
|
||||||
const errorMsg = detail.errors.length > 0 ? detail.errors[0] : '-'
|
const errorMsg = detail.errors.length > 0 ? detail.errors[0] : '-'
|
||||||
|
const retryInfo = detail.retryCount > 0 ? ` [重试${detail.retryCount}次]` : ''
|
||||||
lines.push(
|
lines.push(
|
||||||
`| ${orderNum} | \`${detail.orderNumber}\` | ${detail.materialsDeleted} | ${detail.materialsSkipped} | ${status} | \`${errorMsg}\` |`
|
`| ${orderNum} | \`${detail.orderNumber}\` | ${detail.materialsDeleted} | ${detail.materialsSkipped} | ${status}${retryInfo} | \`${errorMsg}\` |`
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -175,6 +194,62 @@ export class CleanerReportGenerator {
|
|||||||
lines.push('')
|
lines.push('')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add retry details section
|
||||||
|
if (result.retriedOrders > 0) {
|
||||||
|
lines.push('## 重试执行详情')
|
||||||
|
lines.push('')
|
||||||
|
lines.push(
|
||||||
|
`**重试订单总数**: \`${result.retriedOrders}\` | **成功**: \`${result.successfulRetries}\` | **失败**: \`${result.retriedOrders - result.successfulRetries}\``
|
||||||
|
)
|
||||||
|
lines.push('')
|
||||||
|
|
||||||
|
const retriedDetails = result.details.filter((d) => d.retryCount > 0)
|
||||||
|
|
||||||
|
if (retriedDetails.length > 0) {
|
||||||
|
lines.push('### 重试订单列表')
|
||||||
|
lines.push('')
|
||||||
|
lines.push('| 订单号 | 重试次数 | 重试结果 | 重试时间 |')
|
||||||
|
lines.push('| -------- | -------- | -------- | ------------ |')
|
||||||
|
|
||||||
|
retriedDetails.forEach((detail) => {
|
||||||
|
const retryStatus = detail.retrySuccess ? '✅ 成功' : '❌ 失败'
|
||||||
|
const retryTime = detail.retriedAt ? this.formatDateTime(detail.retriedAt) : '-'
|
||||||
|
lines.push(
|
||||||
|
`| \`${detail.orderNumber}\` | ${detail.retryCount} | ${retryStatus} | ${retryTime} |`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
lines.push('')
|
||||||
|
lines.push('### 重试尝试详细记录')
|
||||||
|
lines.push('')
|
||||||
|
|
||||||
|
retriedDetails.forEach((detail) => {
|
||||||
|
lines.push(`#### \`${detail.orderNumber}\``)
|
||||||
|
lines.push('')
|
||||||
|
lines.push(`- **重试次数**: ${detail.retryCount}`)
|
||||||
|
lines.push(`- **最终结果**: ${detail.retrySuccess ? '✅ 成功' : '❌ 失败'}`)
|
||||||
|
|
||||||
|
if (detail.retryAttempts && detail.retryAttempts.length > 0) {
|
||||||
|
lines.push('')
|
||||||
|
lines.push('**重试尝试记录**:')
|
||||||
|
lines.push('')
|
||||||
|
detail.retryAttempts.forEach((attempt, idx) => {
|
||||||
|
lines.push(
|
||||||
|
`${idx + 1}. **第${attempt.attempt}次尝试** - ${this.formatDateTime(attempt.timestamp)}`
|
||||||
|
)
|
||||||
|
lines.push(` - 错误:${attempt.error}`)
|
||||||
|
})
|
||||||
|
lines.push('')
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push('---')
|
||||||
|
lines.push('')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push('')
|
||||||
|
}
|
||||||
|
|
||||||
lines.push(`**报告生成时间**: \`${this.formatDateTime(options.endTime)}\``)
|
lines.push(`**报告生成时间**: \`${this.formatDateTime(options.endTime)}\``)
|
||||||
lines.push('**报表版本**: `v1.0`')
|
lines.push('**报表版本**: `v1.0`')
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,9 @@ export interface CleanerResult {
|
|||||||
materialsSkipped: number
|
materialsSkipped: number
|
||||||
errors: string[]
|
errors: string[]
|
||||||
details: OrderCleanDetail[]
|
details: OrderCleanDetail[]
|
||||||
|
// Retry statistics
|
||||||
|
retriedOrders: number
|
||||||
|
successfulRetries: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SkippedMaterial {
|
export interface SkippedMaterial {
|
||||||
@@ -34,12 +37,23 @@ export interface SkippedMaterial {
|
|||||||
reason: string
|
reason: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RetryAttempt {
|
||||||
|
attempt: number
|
||||||
|
error: string
|
||||||
|
timestamp: number
|
||||||
|
}
|
||||||
|
|
||||||
export interface OrderCleanDetail {
|
export interface OrderCleanDetail {
|
||||||
orderNumber: string
|
orderNumber: string
|
||||||
materialsDeleted: number
|
materialsDeleted: number
|
||||||
materialsSkipped: number
|
materialsSkipped: number
|
||||||
errors: string[]
|
errors: string[]
|
||||||
skippedMaterials: SkippedMaterial[]
|
skippedMaterials: SkippedMaterial[]
|
||||||
|
// Retry-related fields
|
||||||
|
retryCount: number
|
||||||
|
retryAttempts?: RetryAttempt[]
|
||||||
|
retriedAt?: number
|
||||||
|
retrySuccess?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -34,6 +34,9 @@ interface ExecutionReportDialogProps {
|
|||||||
progress?: CleanerProgress | null
|
progress?: CleanerProgress | null
|
||||||
startTime?: number | null
|
startTime?: number | null
|
||||||
triggerRef?: React.RefObject<HTMLElement | null>
|
triggerRef?: React.RefObject<HTMLElement | null>
|
||||||
|
// Retry-related props
|
||||||
|
retriedOrders?: number
|
||||||
|
successfulRetries?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ExecutionReportDialog: React.FC<ExecutionReportDialogProps> = ({
|
export const ExecutionReportDialog: React.FC<ExecutionReportDialogProps> = ({
|
||||||
@@ -47,11 +50,14 @@ export const ExecutionReportDialog: React.FC<ExecutionReportDialogProps> = ({
|
|||||||
isExecuting = false,
|
isExecuting = false,
|
||||||
progress = null,
|
progress = null,
|
||||||
startTime = null,
|
startTime = null,
|
||||||
triggerRef
|
triggerRef,
|
||||||
|
retriedOrders = 0,
|
||||||
|
successfulRetries = 0
|
||||||
}) => {
|
}) => {
|
||||||
const [now, setNow] = React.useState(() => Date.now())
|
const [now, setNow] = React.useState(() => Date.now())
|
||||||
|
|
||||||
const hasErrors = errors.length > 0
|
const hasErrors = errors.length > 0
|
||||||
|
const hasRetries = retriedOrders > 0
|
||||||
const showProgress = isExecuting && progress
|
const showProgress = isExecuting && progress
|
||||||
const isProgressing = !!showProgress
|
const isProgressing = !!showProgress
|
||||||
|
|
||||||
@@ -259,6 +265,46 @@ export const ExecutionReportDialog: React.FC<ExecutionReportDialogProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{hasRetries && (
|
||||||
|
<>
|
||||||
|
<div className="bg-gray-50 rounded-lg p-3 flex items-center gap-3 border border-gray-200">
|
||||||
|
<div className="w-9 h-9 rounded-lg bg-purple-50 flex items-center justify-center flex-shrink-0">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
className="text-purple-600"
|
||||||
|
>
|
||||||
|
<path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" />
|
||||||
|
<path d="M3 3v5h5" />
|
||||||
|
<path d="M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16" />
|
||||||
|
<path d="M16 21h5v-5" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="text-xs text-gray-600">重试订单</div>
|
||||||
|
<div className="text-xl font-semibold text-gray-900">{retriedOrders}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-gray-50 rounded-lg p-3 flex items-center gap-3 border border-gray-200">
|
||||||
|
<div className="w-9 h-9 rounded-lg bg-emerald-50 flex items-center justify-center flex-shrink-0">
|
||||||
|
<CheckCircle size={20} className="text-emerald-600" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="text-xs text-gray-600">成功重试</div>
|
||||||
|
<div className="text-xl font-semibold text-gray-900">{successfulRetries}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{hasErrors && (
|
{hasErrors && (
|
||||||
|
|||||||
@@ -107,11 +107,7 @@ export function ConfirmDialog({
|
|||||||
<Button variant="secondary" onClick={onCancel}>
|
<Button variant="secondary" onClick={onCancel}>
|
||||||
{cancelText}
|
{cancelText}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button data-autofocus="true" variant={styles.buttonVariant} onClick={onConfirm}>
|
||||||
data-autofocus="true"
|
|
||||||
variant={styles.buttonVariant}
|
|
||||||
onClick={onConfirm}
|
|
||||||
>
|
|
||||||
{confirmText}
|
{confirmText}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -125,14 +121,14 @@ export function ConfirmDialog({
|
|||||||
*/
|
*/
|
||||||
export function useConfirmDialog() {
|
export function useConfirmDialog() {
|
||||||
const [config, setConfig] = useState<
|
const [config, setConfig] = useState<
|
||||||
(Omit<ConfirmDialogProps, 'isOpen' | 'onConfirm' | 'onCancel'> & {
|
| (Omit<ConfirmDialogProps, 'isOpen' | 'onConfirm' | 'onCancel'> & {
|
||||||
resolve: (value: boolean) => void
|
resolve: (value: boolean) => void
|
||||||
}) | null>(null)
|
})
|
||||||
|
| null
|
||||||
|
>(null)
|
||||||
|
|
||||||
const confirm = useCallback(
|
const confirm = useCallback(
|
||||||
(
|
(options: Omit<ConfirmDialogProps, 'isOpen' | 'onConfirm' | 'onCancel'>): Promise<boolean> => {
|
||||||
options: Omit<ConfirmDialogProps, 'isOpen' | 'onConfirm' | 'onCancel'>
|
|
||||||
): Promise<boolean> => {
|
|
||||||
return new Promise<boolean>((resolve) => {
|
return new Promise<boolean>((resolve) => {
|
||||||
setConfig({
|
setConfig({
|
||||||
...options,
|
...options,
|
||||||
|
|||||||
@@ -65,6 +65,8 @@ export function useCleaner() {
|
|||||||
materialsDeleted: number
|
materialsDeleted: number
|
||||||
materialsSkipped: number
|
materialsSkipped: number
|
||||||
errors: string[]
|
errors: string[]
|
||||||
|
retriedOrders?: number
|
||||||
|
successfulRetries?: number
|
||||||
} | null>(null)
|
} | null>(null)
|
||||||
|
|
||||||
// Progress state
|
// Progress state
|
||||||
@@ -426,7 +428,9 @@ export function useCleaner() {
|
|||||||
ordersProcessed: cleanerRunData.ordersProcessed,
|
ordersProcessed: cleanerRunData.ordersProcessed,
|
||||||
materialsDeleted: cleanerRunData.materialsDeleted,
|
materialsDeleted: cleanerRunData.materialsDeleted,
|
||||||
materialsSkipped: cleanerRunData.materialsSkipped,
|
materialsSkipped: cleanerRunData.materialsSkipped,
|
||||||
errors: cleanerRunData.errors
|
errors: cleanerRunData.errors,
|
||||||
|
retriedOrders: cleanerRunData.retriedOrders,
|
||||||
|
successfulRetries: cleanerRunData.successfulRetries
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
throw new Error(response.error || '清理失败')
|
throw new Error(response.error || '清理失败')
|
||||||
|
|||||||
@@ -239,7 +239,9 @@ export function useDialogFocus(options: UseDialogFocusOptions): UseDialogFocusRe
|
|||||||
|
|
||||||
if (style.visibility === 'hidden') {
|
if (style.visibility === 'hidden') {
|
||||||
if (import.meta.env.DEV) {
|
if (import.meta.env.DEV) {
|
||||||
console.warn('[useDialogFocus] Trigger element is visibility: hidden, cannot restore focus')
|
console.warn(
|
||||||
|
'[useDialogFocus] Trigger element is visibility: hidden, cannot restore focus'
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -496,6 +496,8 @@ const CleanerPage: React.FC = () => {
|
|||||||
progress={progress}
|
progress={progress}
|
||||||
startTime={startTime}
|
startTime={startTime}
|
||||||
triggerRef={executeButtonRef}
|
triggerRef={executeButtonRef}
|
||||||
|
retriedOrders={reportData?.retriedOrders}
|
||||||
|
successfulRetries={reportData?.successfulRetries}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Confirmation Dialog */}
|
{/* Confirmation Dialog */}
|
||||||
|
|||||||
Reference in New Issue
Block a user