1+ function deriveSyncStartedAtMs ( syncStartedAtMs , backendElapsedSeconds , nowMs ) {
2+ if ( syncStartedAtMs != null ) return syncStartedAtMs ;
3+ const elapsedSeconds = Math . max ( 0 , Number ( backendElapsedSeconds ) || 0 ) ;
4+ if ( elapsedSeconds > 0 ) return nowMs - elapsedSeconds * 1000 ;
5+ return nowMs ;
6+ }
7+
8+ function computeDisplayedElapsedSeconds ( syncStartedAtMs , backendElapsedSeconds , nowMs ) {
9+ const elapsedSeconds = Math . max ( 0 , Number ( backendElapsedSeconds ) || 0 ) ;
10+ if ( syncStartedAtMs == null ) return elapsedSeconds ;
11+ return Math . max ( elapsedSeconds , Math . max ( 0 , ( nowMs - syncStartedAtMs ) / 1000 ) ) ;
12+ }
13+
114// Alpine.js 数据组件
215// 等待 alpine:init 事件注册组件,再等待 pywebviewready 后初始化数据
316document . addEventListener ( 'alpine:init' , ( ) => {
@@ -55,6 +68,9 @@ document.addEventListener('alpine:init', () => {
5568 completed : 0 , // 已完成产品数
5669 total : 0 , // 总产品数
5770 elapsedSeconds : 0 , // 已用秒数
71+ displayElapsedSeconds : 0 , // 顶部展示用耗时,可在前端平滑递增
72+ pausedDisplayElapsedSeconds : null , // confirm_needed 时冻结的展示耗时
73+ syncStartedAtMs : null , // 前端本地记录的同步起点,用于平滑显示总耗时
5874 errorMessage : '' , // 错误信息(error 状态时)
5975 runSummary : null , // 完成后的摘要对象
6076 pollTimer : null , // setTimeout 句柄
@@ -178,6 +194,9 @@ document.addEventListener('alpine:init', () => {
178194 this . total = 0 ;
179195 this . currentProduct = '' ;
180196 this . elapsedSeconds = 0 ;
197+ this . displayElapsedSeconds = 0 ;
198+ this . pausedDisplayElapsedSeconds = null ;
199+ this . syncStartedAtMs = null ;
181200 this . errorMessage = '' ;
182201 this . runSummary = null ;
183202 this . syncProducts = [ ] ;
@@ -189,16 +208,19 @@ document.addEventListener('alpine:init', () => {
189208 try {
190209 const result = await window . pywebview . api . start_sync ( ) ;
191210 if ( result . started ) {
211+ this . syncStartedAtMs = Date . now ( ) ;
192212 this . startPolling ( ) ;
193213 } else {
194214 // Python 端拒绝启动(如已有任务在跑)
195215 this . errorMessage = result . message || '无法启动同步' ;
196216 this . syncStatus = 'error' ;
217+ this . syncStartedAtMs = null ;
197218 }
198219 } catch ( e ) {
199220 console . error ( 'startSync failed:' , e ) ;
200221 this . errorMessage = String ( e ) ;
201222 this . syncStatus = 'error' ;
223+ this . syncStartedAtMs = null ;
202224 }
203225 } ,
204226
@@ -211,6 +233,9 @@ document.addEventListener('alpine:init', () => {
211233 this . total = 0 ;
212234 this . currentProduct = '' ;
213235 this . elapsedSeconds = 0 ;
236+ this . displayElapsedSeconds = 0 ;
237+ this . pausedDisplayElapsedSeconds = null ;
238+ this . syncStartedAtMs = null ;
214239 this . errorMessage = '' ;
215240 this . runSummary = null ;
216241 this . syncProducts = [ ] ;
@@ -223,15 +248,18 @@ document.addEventListener('alpine:init', () => {
223248 // true 表示仅重试失败产品
224249 const result = await window . pywebview . api . start_sync ( true ) ;
225250 if ( result . started ) {
251+ this . syncStartedAtMs = Date . now ( ) ;
226252 this . startPolling ( ) ;
227253 } else {
228254 this . errorMessage = result . message || '无法启动同步' ;
229255 this . syncStatus = 'error' ;
256+ this . syncStartedAtMs = null ;
230257 }
231258 } catch ( e ) {
232259 console . error ( 'retryFailed failed:' , e ) ;
233260 this . errorMessage = String ( e ) ;
234261 this . syncStatus = 'error' ;
262+ this . syncStartedAtMs = null ;
235263 }
236264 } ,
237265
@@ -252,7 +280,7 @@ document.addEventListener('alpine:init', () => {
252280 this . currentProduct = p . current_product || '' ;
253281 this . completed = p . completed || 0 ;
254282 this . total = p . total || 0 ;
255- this . elapsedSeconds = p . elapsed_seconds || 0 ;
283+ this . syncElapsedFromProgress ( p ) ;
256284
257285 // 更新产品列表和全部产品名
258286 if ( p . products && Array . isArray ( p . products ) ) {
@@ -266,7 +294,8 @@ document.addEventListener('alpine:init', () => {
266294 if ( p . status === 'confirm_needed' && p . estimate ) {
267295 this . estimateData = p . estimate ;
268296 this . postprocessing = false ;
269- // 不切换 syncStatus,继续轮询等待用户点击确认/取消
297+ this . pollTimer = null ;
298+ return ; // 等待用户确认,不再继续轮询
270299 } else if ( p . status === 'postprocessing' ) {
271300 this . postprocessing = true ;
272301 this . postprocessDetail = p . postprocess_detail || '' ;
@@ -275,6 +304,7 @@ document.addEventListener('alpine:init', () => {
275304 this . postprocessing = false ;
276305 this . postprocessDetail = '' ;
277306 this . estimateData = null ;
307+ this . syncStartedAtMs = null ;
278308 this . runSummary = p . run_summary ;
279309 this . historyLoaded = false ; // 有新运行,下次切历史页时刷新
280310 this . checkUpdateResult = null ; // 同步后清除检查更新结果
@@ -285,12 +315,21 @@ document.addEventListener('alpine:init', () => {
285315 this . postprocessing = false ;
286316 this . postprocessDetail = '' ;
287317 this . estimateData = null ;
318+ this . syncStartedAtMs = null ;
288319 this . errorMessage = p . error_message || '同步失败' ;
289320 this . runSummary = p . run_summary ; // 部分失败时也携带摘要
290321 this . historyLoaded = false ;
291322 this . checkUpdateResult = null ;
292323 this . pollTimer = null ;
293324 return ; // 终态,不再调度下次轮询
325+ } else if ( p . status === 'idle' ) {
326+ this . syncStatus = 'idle' ;
327+ this . postprocessing = false ;
328+ this . postprocessDetail = '' ;
329+ this . estimateData = null ;
330+ this . syncStartedAtMs = null ;
331+ this . pollTimer = null ;
332+ return ; // 终态,不再调度下次轮询
294333 }
295334 } catch ( e ) {
296335 console . error ( 'poll failed:' , e ) ;
@@ -318,11 +357,15 @@ document.addEventListener('alpine:init', () => {
318357 this . completed = 0 ;
319358 this . total = 0 ;
320359 this . elapsedSeconds = 0 ;
360+ this . displayElapsedSeconds = 0 ;
361+ this . pausedDisplayElapsedSeconds = null ;
362+ this . syncStartedAtMs = null ;
321363 this . errorMessage = '' ;
322364 this . runSummary = null ;
323365 this . syncProducts = [ ] ;
324366 this . allProducts = [ ] ;
325367 this . estimateData = null ;
368+ this . pausedDisplayElapsedSeconds = null ;
326369 } ,
327370
328371 // ===== API 调用量确认 =====
@@ -331,10 +374,13 @@ document.addEventListener('alpine:init', () => {
331374 async confirmSync ( ) {
332375 try {
333376 await window . pywebview . api . confirm_sync ( ) ;
377+ this . estimateData = null ;
378+ this . pausedDisplayElapsedSeconds = null ;
379+ this . syncStartedAtMs = Date . now ( ) - this . displayElapsedSeconds * 1000 ;
380+ this . startPolling ( ) ;
334381 } catch ( e ) {
335382 console . error ( 'confirmSync failed:' , e ) ;
336383 }
337- this . estimateData = null ;
338384 } ,
339385
340386 // 用户点击"取消":通知后台线程取消,await 完成后再切状态(避免请求期间状态已变)
@@ -346,10 +392,46 @@ document.addEventListener('alpine:init', () => {
346392 } finally {
347393 this . estimateData = null ;
348394 this . syncStatus = 'idle' ;
395+ this . pausedDisplayElapsedSeconds = null ;
396+ this . syncStartedAtMs = null ;
349397 this . stopPolling ( ) ;
350398 }
351399 } ,
352400
401+ syncElapsedFromProgress ( progress ) {
402+ const backendElapsedSeconds = progress && progress . elapsed_seconds != null
403+ ? progress . elapsed_seconds
404+ : 0 ;
405+ const status = progress && progress . status ? progress . status : '' ;
406+ this . elapsedSeconds = backendElapsedSeconds ;
407+ if ( status === 'confirm_needed' ) {
408+ if ( this . pausedDisplayElapsedSeconds == null ) {
409+ this . pausedDisplayElapsedSeconds = this . displayElapsedSeconds > 0
410+ ? this . displayElapsedSeconds
411+ : backendElapsedSeconds ;
412+ }
413+ this . displayElapsedSeconds = this . pausedDisplayElapsedSeconds ;
414+ this . syncStartedAtMs = null ;
415+ return ;
416+ }
417+ this . pausedDisplayElapsedSeconds = null ;
418+ if ( status === 'done' || status === 'error' || status === 'idle' ) {
419+ this . displayElapsedSeconds = backendElapsedSeconds ;
420+ return ;
421+ }
422+ const nowMs = Date . now ( ) ;
423+ this . syncStartedAtMs = deriveSyncStartedAtMs (
424+ this . syncStartedAtMs ,
425+ backendElapsedSeconds ,
426+ nowMs ,
427+ ) ;
428+ this . displayElapsedSeconds = computeDisplayedElapsedSeconds (
429+ this . syncStartedAtMs ,
430+ backendElapsedSeconds ,
431+ nowMs ,
432+ ) ;
433+ } ,
434+
353435 // ===== 格式化工具函数 =====
354436
355437 // 进度百分比,total=0 时返回 0 避免除零
0 commit comments