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 class="cip-track__fill" :style="{ width: store.percent + '%' }" />
|
||||||
</div>
|
</div>
|
||||||
<div class="cip-percent">{{ store.percent }}٪</div>
|
<div class="cip-percent">{{ store.percent }}٪</div>
|
||||||
|
<div v-if="etaLabel" class="cip-eta">{{ etaLabel }}</div>
|
||||||
<div class="cip-current">
|
<div class="cip-current">
|
||||||
<a-spin size="small" />
|
<a-spin size="small" />
|
||||||
<span class="cip-current__name">{{ store.state.current || '...' }}</span>
|
<span class="cip-current__name">{{ store.state.current || '...' }}</span>
|
||||||
@@ -127,6 +128,7 @@
|
|||||||
</template>
|
</template>
|
||||||
<template v-else>{{ store.state.processed }} از {{ store.state.total }}</template>
|
<template v-else>{{ store.state.processed }} از {{ store.state.total }}</template>
|
||||||
</span>
|
</span>
|
||||||
|
<span v-if="store.active && etaLabel" class="cip-mini__eta">{{ etaLabel }}</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="cip-mini__bar">
|
<span class="cip-mini__bar">
|
||||||
<span class="cip-mini__bar-fill" :style="{ width: store.percent + '%' }" />
|
<span class="cip-mini__bar-fill" :style="{ width: store.percent + '%' }" />
|
||||||
@@ -137,12 +139,87 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<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 Icon from '@core/components/BaseIcon/src/Icon.vue'
|
||||||
import { useTahesabDocSyncStore } from '@/store/tahesabDocSyncStore'
|
import { useTahesabDocSyncStore } from '@/store/tahesabDocSyncStore'
|
||||||
|
|
||||||
const store = useTahesabDocSyncStore()
|
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)
|
const stage = computed(() => store.stage)
|
||||||
|
|
||||||
function outcomeMap() {
|
function outcomeMap() {
|
||||||
@@ -217,6 +294,10 @@ const ringStyle = computed(() => ({
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
void store.hydrate()
|
void store.hydrate()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
stopTick()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="less">
|
<style scoped lang="less">
|
||||||
@@ -360,6 +441,15 @@ onMounted(() => {
|
|||||||
color: var(--admin-text-secondary);
|
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 {
|
.cip-current {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -622,6 +712,13 @@ onMounted(() => {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__eta {
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--admin-text-muted);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
&__bar {
|
&__bar {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 14px;
|
left: 14px;
|
||||||
|
|||||||
Reference in New Issue
Block a user