feat: add estimated completion time to cleaner progress dialog

- Display remaining time estimate (in minutes) when progress >= 5%
- Show estimated completion time in 12-hour format (上午/下午 HH:MM:SS)
- Update every second during execution
- Add startTime state tracking in useCleaner hook
This commit is contained in:
Misaka_Company
2026-03-05 14:17:34 +08:00
parent 49ac29e70c
commit 7d2cf934f8
3 changed files with 91 additions and 2 deletions

View File

@@ -31,6 +31,7 @@ interface ExecutionReportDialogProps {
dryRun?: boolean dryRun?: boolean
isExecuting?: boolean isExecuting?: boolean
progress?: CleanerProgress | null progress?: CleanerProgress | null
startTime?: number | null
} }
export const ExecutionReportDialog: React.FC<ExecutionReportDialogProps> = ({ export const ExecutionReportDialog: React.FC<ExecutionReportDialogProps> = ({
@@ -42,15 +43,55 @@ export const ExecutionReportDialog: React.FC<ExecutionReportDialogProps> = ({
errors = [], errors = [],
dryRun = false, dryRun = false,
isExecuting = false, isExecuting = false,
progress = null progress = null,
startTime = null
}) => { }) => {
if (!isOpen) return null if (!isOpen) return null
const hasErrors = errors.length > 0 const hasErrors = errors.length > 0
const showProgress = isExecuting && progress const showProgress = isExecuting && progress
const [now, setNow] = React.useState(Date.now())
React.useEffect(() => {
if (!showProgress || !startTime) return
const interval = setInterval(() => {
setNow(Date.now())
}, 1000)
return () => clearInterval(interval)
}, [showProgress, startTime])
const estimatedTime = React.useMemo(() => {
if (!showProgress || !startTime || !progress) return null
const currentProgress = progress.progress
if (currentProgress < 5) return null
const elapsedMs = now - startTime
const elapsedMinutes = elapsedMs / 60000
const totalEstimatedMinutes = elapsedMinutes / (currentProgress / 100)
const remainingMinutes = Math.max(0, totalEstimatedMinutes - elapsedMinutes)
const remainingRounded = Math.round(remainingMinutes)
const endTime = new Date(now + remainingMinutes * 60000)
const hours = endTime.getHours()
const minutes = endTime.getMinutes()
const seconds = endTime.getSeconds()
const period = hours >= 12 ? '下午' : '上午'
const displayHours = hours % 12 || 12
const formattedTime = `${period} ${displayHours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`
return {
remainingMinutes: remainingRounded,
formattedTime
}
}, [showProgress, startTime, progress, now])
return ( return (
<div className="execution-report-overlay" onClick={onClose}> <div className="execution-report-overlay" onClick={showProgress ? undefined : onClose}>
<div <div
className="execution-report-dialog" className="execution-report-dialog"
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
@@ -130,6 +171,20 @@ export const ExecutionReportDialog: React.FC<ExecutionReportDialogProps> = ({
<span className="order-number">{progress.currentOrderNumber}</span> <span className="order-number">{progress.currentOrderNumber}</span>
</div> </div>
)} )}
{estimatedTime && (
<div className="estimated-time-info">
<div className="estimated-time-item">
<span className="time-label"></span>
<span className="time-value">{estimatedTime.remainingMinutes} </span>
</div>
<div className="estimated-time-item">
<span className="time-label"></span>
<span className="time-value">{estimatedTime.formattedTime}</span>
<span className="time-label"></span>
</div>
</div>
)}
</div> </div>
) : ( ) : (
// Result View Content // Result View Content
@@ -443,6 +498,34 @@ export const ExecutionReportDialog: React.FC<ExecutionReportDialogProps> = ({
font-family: monospace; font-family: monospace;
} }
.estimated-time-info {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
padding: 12px;
background: #f6ffed;
border-radius: 6px;
border: 1px solid #b7eb8f;
}
.estimated-time-item {
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
}
.estimated-time-item .time-label {
color: #666;
}
.estimated-time-item .time-value {
font-size: 14px;
font-weight: 600;
color: #52c41a;
}
/* Result View Styles */ /* Result View Styles */
.stats-grid { .stats-grid {
display: grid; display: grid;

View File

@@ -67,6 +67,7 @@ export function useCleaner() {
// Progress state // Progress state
const [progress, setProgress] = useState<CleanerProgress | null>(null) const [progress, setProgress] = useState<CleanerProgress | null>(null)
const [startTime, setStartTime] = useState<number | null>(null)
// Execution settings state // Execution settings state
const [headless, setHeadless] = useState(() => { const [headless, setHeadless] = useState(() => {
@@ -316,6 +317,7 @@ export function useCleaner() {
setIsRunning(true) setIsRunning(true)
setIsExecuting(true) setIsExecuting(true)
setProgress(null) setProgress(null)
setStartTime(Date.now())
setIsReportDialogOpen(true) setIsReportDialogOpen(true)
try { try {
@@ -355,6 +357,7 @@ export function useCleaner() {
setIsRunning(false) setIsRunning(false)
setIsExecuting(false) setIsExecuting(false)
setProgress(null) setProgress(null)
setStartTime(null)
} }
} }
@@ -430,6 +433,7 @@ export function useCleaner() {
cancelEdit, cancelEdit,
handleAssignManagerOnSelect, handleAssignManagerOnSelect,
progress, progress,
startTime,
handleValidation, handleValidation,
handleCheckboxToggle, handleCheckboxToggle,
handleConfirmDeletion, handleConfirmDeletion,

View File

@@ -57,6 +57,7 @@ const CleanerPage: React.FC = () => {
cancelEdit, cancelEdit,
handleAssignManagerOnSelect, handleAssignManagerOnSelect,
progress, progress,
startTime,
handleValidation, handleValidation,
handleCheckboxToggle, handleCheckboxToggle,
handleConfirmDeletion, handleConfirmDeletion,
@@ -485,6 +486,7 @@ const CleanerPage: React.FC = () => {
dryRun={dryRun} dryRun={dryRun}
isExecuting={isExecuting} isExecuting={isExecuting}
progress={progress} progress={progress}
startTime={startTime}
/> />
</div> </div>
) )