feat(tahesab): show remaining-time estimate in sync progress modal
Estimate ETA from processed throughput for customer export and document sync. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -38,6 +38,7 @@
|
||||
<div class="cip-track__fill" :style="{ width: store.percent + '%' }" />
|
||||
</div>
|
||||
<div class="cip-percent">{{ store.percent }}٪</div>
|
||||
<div v-if="etaLabel" class="cip-eta">{{ etaLabel }}</div>
|
||||
<div class="cip-current">
|
||||
<a-spin size="small" />
|
||||
<span class="cip-current__name">{{ store.state.current || '...' }}</span>
|
||||
@@ -127,6 +128,7 @@
|
||||
</template>
|
||||
<template v-else>{{ store.state.processed }} از {{ store.state.total }}</template>
|
||||
</span>
|
||||
<span v-if="store.active && etaLabel" class="cip-mini__eta">{{ etaLabel }}</span>
|
||||
</span>
|
||||
<span class="cip-mini__bar">
|
||||
<span class="cip-mini__bar-fill" :style="{ width: store.percent + '%' }" />
|
||||
@@ -137,12 +139,87 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted } from 'vue'
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import Icon from '@core/components/BaseIcon/src/Icon.vue'
|
||||
import { useTahesabDocSyncStore } from '@/store/tahesabDocSyncStore'
|
||||
|
||||
const store = useTahesabDocSyncStore()
|
||||
|
||||
const runStartedAt = ref<number | null>(null)
|
||||
const runStartProcessed = ref(0)
|
||||
const tick = ref(Date.now())
|
||||
let tickTimer: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
function formatEta(seconds: number): string {
|
||||
if (!Number.isFinite(seconds) || seconds <= 0) return 'بهزودی'
|
||||
const rounded = Math.ceil(seconds)
|
||||
if (rounded < 60) return `حدود ${rounded} ثانیه`
|
||||
const mins = Math.ceil(rounded / 60)
|
||||
if (mins < 60) return `حدود ${mins} دقیقه`
|
||||
const hours = Math.floor(mins / 60)
|
||||
const remMins = mins % 60
|
||||
if (remMins === 0) return `حدود ${hours} ساعت`
|
||||
return `حدود ${hours} ساعت و ${remMins} دقیقه`
|
||||
}
|
||||
|
||||
function resetEtaClock() {
|
||||
runStartedAt.value = Date.now()
|
||||
runStartProcessed.value = store.state.processed
|
||||
}
|
||||
|
||||
function startTick() {
|
||||
if (tickTimer) return
|
||||
tickTimer = setInterval(() => {
|
||||
tick.value = Date.now()
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
function stopTick() {
|
||||
if (!tickTimer) return
|
||||
clearInterval(tickTimer)
|
||||
tickTimer = null
|
||||
}
|
||||
|
||||
watch(
|
||||
() => store.active,
|
||||
(active) => {
|
||||
if (active) {
|
||||
resetEtaClock()
|
||||
startTick()
|
||||
return
|
||||
}
|
||||
runStartedAt.value = null
|
||||
stopTick()
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
watch(
|
||||
() => store.state.stage,
|
||||
() => {
|
||||
if (store.active) resetEtaClock()
|
||||
},
|
||||
)
|
||||
|
||||
const etaLabel = computed(() => {
|
||||
void tick.value
|
||||
if (!store.active) return ''
|
||||
|
||||
const { processed, total } = store.state
|
||||
const remaining = Math.max(0, total - processed)
|
||||
if (remaining <= 0) return 'در حال اتمام…'
|
||||
if (!runStartedAt.value) return ''
|
||||
|
||||
const deltaProcessed = processed - runStartProcessed.value
|
||||
const elapsedSec = (Date.now() - runStartedAt.value) / 1000
|
||||
if (deltaProcessed < 2 || elapsedSec < 4) {
|
||||
return 'در حال محاسبه زمان باقیمانده…'
|
||||
}
|
||||
|
||||
const secPerItem = elapsedSec / deltaProcessed
|
||||
return `تخمین باقیمانده: ${formatEta(secPerItem * remaining)}`
|
||||
})
|
||||
|
||||
const stage = computed(() => store.stage)
|
||||
|
||||
function outcomeMap() {
|
||||
@@ -217,6 +294,10 @@ const ringStyle = computed(() => ({
|
||||
onMounted(() => {
|
||||
void store.hydrate()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
stopTick()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
@@ -360,6 +441,15 @@ onMounted(() => {
|
||||
color: var(--admin-text-secondary);
|
||||
}
|
||||
|
||||
.cip-eta {
|
||||
align-self: flex-end;
|
||||
margin-top: 4px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--admin-text-muted);
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.cip-current {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -622,6 +712,13 @@ onMounted(() => {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__eta {
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
color: var(--admin-text-muted);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__bar {
|
||||
position: absolute;
|
||||
left: 14px;
|
||||
|
||||
Reference in New Issue
Block a user