From 7d2cf934f810e7d837bf30cfcd8392ba7c2cd66e Mon Sep 17 00:00:00 2001 From: Misaka_Company Date: Thu, 5 Mar 2026 14:17:34 +0800 Subject: [PATCH] feat: add estimated completion time to cleaner progress dialog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .../src/components/ExecutionReportDialog.tsx | 87 ++++++++++++++++++- src/renderer/src/hooks/useCleaner.ts | 4 + src/renderer/src/pages/CleanerPage.tsx | 2 + 3 files changed, 91 insertions(+), 2 deletions(-) diff --git a/src/renderer/src/components/ExecutionReportDialog.tsx b/src/renderer/src/components/ExecutionReportDialog.tsx index 9a4d12a..562a8fa 100644 --- a/src/renderer/src/components/ExecutionReportDialog.tsx +++ b/src/renderer/src/components/ExecutionReportDialog.tsx @@ -31,6 +31,7 @@ interface ExecutionReportDialogProps { dryRun?: boolean isExecuting?: boolean progress?: CleanerProgress | null + startTime?: number | null } export const ExecutionReportDialog: React.FC = ({ @@ -42,15 +43,55 @@ export const ExecutionReportDialog: React.FC = ({ errors = [], dryRun = false, isExecuting = false, - progress = null + progress = null, + startTime = null }) => { if (!isOpen) return null const hasErrors = errors.length > 0 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 ( -
+
e.stopPropagation()} @@ -130,6 +171,20 @@ export const ExecutionReportDialog: React.FC = ({ {progress.currentOrderNumber}
)} + + {estimatedTime && ( +
+
+ 预估还要 + {estimatedTime.remainingMinutes} 分钟 +
+
+ 将会在 + {estimatedTime.formattedTime} + 执行完毕 +
+
+ )}
) : ( // Result View Content @@ -443,6 +498,34 @@ export const ExecutionReportDialog: React.FC = ({ 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 */ .stats-grid { display: grid; diff --git a/src/renderer/src/hooks/useCleaner.ts b/src/renderer/src/hooks/useCleaner.ts index 35d4729..b392c8c 100644 --- a/src/renderer/src/hooks/useCleaner.ts +++ b/src/renderer/src/hooks/useCleaner.ts @@ -67,6 +67,7 @@ export function useCleaner() { // Progress state const [progress, setProgress] = useState(null) + const [startTime, setStartTime] = useState(null) // Execution settings state const [headless, setHeadless] = useState(() => { @@ -316,6 +317,7 @@ export function useCleaner() { setIsRunning(true) setIsExecuting(true) setProgress(null) + setStartTime(Date.now()) setIsReportDialogOpen(true) try { @@ -355,6 +357,7 @@ export function useCleaner() { setIsRunning(false) setIsExecuting(false) setProgress(null) + setStartTime(null) } } @@ -430,6 +433,7 @@ export function useCleaner() { cancelEdit, handleAssignManagerOnSelect, progress, + startTime, handleValidation, handleCheckboxToggle, handleConfirmDeletion, diff --git a/src/renderer/src/pages/CleanerPage.tsx b/src/renderer/src/pages/CleanerPage.tsx index 70f4967..9e48051 100644 --- a/src/renderer/src/pages/CleanerPage.tsx +++ b/src/renderer/src/pages/CleanerPage.tsx @@ -57,6 +57,7 @@ const CleanerPage: React.FC = () => { cancelEdit, handleAssignManagerOnSelect, progress, + startTime, handleValidation, handleCheckboxToggle, handleConfirmDeletion, @@ -485,6 +486,7 @@ const CleanerPage: React.FC = () => { dryRun={dryRun} isExecuting={isExecuting} progress={progress} + startTime={startTime} />
)