优化订单数源的进度条

This commit is contained in:
wuweihua
2026-11-05 15:58:20 +08:00
parent 25801fe42f
commit 93c081775d
2 changed files with 117 additions and 44 deletions
@@ -2,12 +2,11 @@
<el-dialog
v-model="visible"
:title="dialogTitle"
width="520px"
width="480px"
append-to-body
destroy-on-close
:close-on-click-modal="!loading"
:close-on-press-escape="!loading"
:show-close="!loading"
:close-on-click-modal="false"
:close-on-press-escape="true"
:show-close="true"
@closed="onClosed"
>
<el-form v-show="!loading && !finished" label-width="96px">
@@ -28,7 +27,7 @@
/>
</el-form>
<div v-if="loading || finished" class="filter-retry-progress">
<div v-if="loading || finished" class="sync-progress">
<el-progress
:percentage="progressPercent"
:status="progressStatus"
@@ -36,15 +35,14 @@
striped
:striped-flow="loading"
/>
<div class="filter-retry-progress__msg">{{ progressMessage }}</div>
<div class="filter-retry-progress__text">
<div class="sync-progress__msg">{{ progressMessage }}</div>
<div class="sync-progress__text">
总数 {{ progress.total || 0 }}
/ 已处理 {{ progress.processed || 0 }}
/ 成功 {{ progress.ok || 0 }}
/ 失败 {{ progress.fail || 0 }}
</div>
<div v-if="progress.error" class="filter-retry-progress__error">{{ progress.error }}</div>
<p v-if="loading" class="filter-retry-progress__hint">请勿关闭页面,大量订单同步落库可能需要一些时间</p>
<div v-if="progress.error" class="sync-progress__error">{{ progress.error }}</div>
<p v-if="loading" class="sync-progress__hint">同步在后台进行,可收起弹窗继续操作;请勿关闭或刷新本页</p>
</div>
<template #footer>
@@ -52,11 +50,11 @@
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" @click="submit">开始同步落库</el-button>
</template>
<template v-else-if="finished">
<el-button type="primary" @click="visible = false">关闭</el-button>
<template v-else-if="loading">
<el-button @click="visible = false">收起</el-button>
</template>
<template v-else>
<el-button disabled>执行中...</el-button>
<el-button type="primary" @click="visible = false">关闭</el-button>
</template>
</template>
</el-dialog>
@@ -72,6 +70,9 @@ const visible = ref(false)
const loading = ref(false)
const finished = ref(false)
const query = ref({})
const asyncTaskIdRef = ref('')
const pollGeneration = ref(0)
const autoCollapseTimer = ref(null)
const progress = ref({
percent: 0,
message: '',
@@ -102,7 +103,11 @@ const filterText = computed(() => {
const dialogTitle = computed(() => {
if (loading.value) return '异步同步落库进行中'
if (finished.value) return '异步同步落库完成'
if (finished.value) {
const st = String(progress.value?.status || '').toUpperCase()
if (st === 'FAIL') return '异步同步落库失败'
return '异步同步落库完成'
}
return '条件批量同步落库'
})
@@ -112,6 +117,7 @@ const progressStatus = computed(() => {
const st = (progress.value?.status || '').toUpperCase()
if (st === 'SUCCESS') return 'success'
if (st === 'FAIL') return 'exception'
if (finished.value && (progress.value?.fail || 0) > 0) return 'warning'
return undefined
})
@@ -119,12 +125,15 @@ const progressMessage = computed(() => mapProgressMessage(progress.value?.messag
function mapProgressMessage(message, data) {
const m = message || ''
if (!m || m === 'queued') return '排队中...'
if (m === 'starting') return '正在统计订单...'
if (!m || m === 'queued') return '排队中…'
if (m === 'starting') return '正在统计订单…'
if (m === 'no tasks') return '无匹配订单'
if (m === 'failed') return '执行失败'
if (m.startsWith('processing ')) {
return '正在同步落库 ' + m.slice('processing '.length)
const ok = data?.ok || 0
const total = data?.total || 0
const fail = data?.fail || 0
return '正在同步落库 ' + ok + '/' + total + (fail ? ('(失败 ' + fail + ')') : '')
}
if (m.startsWith('done ')) {
return '完成:成功 ' + (data?.ok || 0) + ' / 失败 ' + (data?.fail || 0)
@@ -147,12 +156,28 @@ function emitProgress() {
})
}
function clearAutoCollapse() {
if (autoCollapseTimer.value) {
clearTimeout(autoCollapseTimer.value)
autoCollapseTimer.value = null
}
}
function scheduleAutoCollapse() {
clearAutoCollapse()
autoCollapseTimer.value = window.setTimeout(() => {
if (loading.value) visible.value = false
}, 600)
}
function open(filterQuery, autoStart) {
// Already running / finished: just reopen progress view (same as order-list rebill).
if (loading.value || finished.value) {
visible.value = true
return
}
query.value = { ...(filterQuery || {}) }
asyncTaskIdRef.value = ''
progress.value = {
percent: 0,
message: '',
@@ -173,7 +198,8 @@ function open(filterQuery, autoStart) {
}
function onClosed() {
if (!loading.value) {
// Keep finished state until next open so float can still reopen detail.
if (!loading.value && !finished.value) {
finished.value = false
}
}
@@ -185,7 +211,6 @@ function submit(skipConfirm) {
const identifiers = Array.isArray(q.orderIdentifiers)
? q.orderIdentifiers.filter(v => v != null && String(v).trim() !== '')
: []
// 与查询同口径:有单号即可同步;无单号时必须客户 + 提货时间
if (!identifiers.length && ((!codes.length && !keywords.length) || !q.beginTime || !q.endTime)) {
proxy.$modal.msgError('请输入单号,或先输入客户并选择提货时间')
return
@@ -204,6 +229,7 @@ function submit(skipConfirm) {
error: ''
}
emitProgress()
scheduleAutoCollapse()
return syncOrderSourceByFilter({
customerCodes: codes.length ? codes : undefined,
customerKeywords: keywords.length ? keywords : undefined,
@@ -220,9 +246,21 @@ function submit(skipConfirm) {
if (!asyncTaskId) {
throw new Error('asyncTaskId missing')
}
return poll(asyncTaskId)
asyncTaskIdRef.value = String(asyncTaskId)
return poll(String(asyncTaskId))
}).then((data) => {
finished.value = true
progress.value = {
...progress.value,
percent: 100,
status: (data && data.status) || 'SUCCESS',
total: data?.total ?? progress.value.total,
processed: data?.processed ?? progress.value.processed,
ok: data?.ok ?? progress.value.ok,
fail: data?.fail ?? progress.value.fail,
message: data?.message || progress.value.message,
error: data?.error || ''
}
emit('success', data)
proxy.$modal.msgSuccess(
'完成:成功 ' + (data.ok || 0) + ' / 失败 ' + (data.fail || 0)
@@ -241,6 +279,7 @@ function submit(skipConfirm) {
}
}
}).finally(() => {
clearAutoCollapse()
loading.value = false
emitProgress()
})
@@ -255,12 +294,28 @@ function submit(skipConfirm) {
).then(() => start()).catch(() => {})
}
/**
* Poll like order-list rebill: 1s visible / 2.5s hidden, skip unchanged payload emits,
* single generation so reopen won't stack loops.
*/
function poll(asyncTaskId) {
const gen = ++pollGeneration.value
let lastSig = ''
return new Promise((resolve, reject) => {
const tick = () => {
if (gen !== pollGeneration.value) {
reject(Object.assign(new Error('sync poll superseded'), { superseded: true }))
return
}
const hidden = typeof document !== 'undefined' && document.visibilityState === 'hidden'
getOrderSourceSyncProgress(asyncTaskId).then(res => {
if (gen !== pollGeneration.value) {
reject(Object.assign(new Error('sync poll superseded'), { superseded: true }))
return
}
const data = res.data || {}
progress.value = {
const st = String(data.status || '').toUpperCase()
const next = {
percent: Number(data.percent) || 0,
message: data.message || '',
total: data.total || 0,
@@ -270,8 +325,12 @@ function poll(asyncTaskId) {
status: data.status || '',
error: data.error || ''
}
emitProgress()
const st = (data.status || '').toUpperCase()
const sig = [st, next.percent, next.ok, next.fail, next.total, next.message].join('|')
progress.value = next
if (sig !== lastSig) {
lastSig = sig
emitProgress()
}
if (st === 'SUCCESS') {
progress.value.percent = 100
resolve(data)
@@ -282,43 +341,46 @@ function poll(asyncTaskId) {
reject(new Error(data.error || 'fail'))
return
}
setTimeout(tick, 1500)
}).catch(err => reject(err))
setTimeout(tick, hidden ? 2500 : 1000)
}).catch(err => {
if (gen !== pollGeneration.value) return
reject(err)
})
}
tick()
})
}
onBeforeUnmount(() => {
clearAutoCollapse()
pollGeneration.value += 1
})
defineExpose({ open })
</script>
<style scoped>
.filter-retry-progress {
.sync-progress {
padding: 8px 4px 0;
}
.filter-retry-progress__msg {
margin-top: 14px;
font-size: 14px;
font-weight: 600;
color: #303133;
line-height: 1.5;
.sync-progress__msg {
margin-top: 12px;
color: var(--el-text-color-regular);
}
.filter-retry-progress__text {
.sync-progress__text {
margin-top: 8px;
font-size: 12px;
color: #606266;
line-height: 1.6;
font-size: 13px;
color: var(--el-text-color-secondary);
}
.filter-retry-progress__error {
.sync-progress__error {
margin-top: 8px;
font-size: 12px;
color: #f56c6c;
word-break: break-all;
}
.filter-retry-progress__hint {
margin: 12px 0 0;
.sync-progress__hint {
margin-top: 12px;
font-size: 12px;
color: #909399;
line-height: 1.5;
color: var(--el-color-warning);
}
</style>
@@ -459,12 +459,17 @@ function onCompareFillSearch(payload) {
}
function openFilterSync() {
// Running / just finished: reopen progress dialog (same as order-list float click).
if (syncFloat.value && (syncFloat.value.loading || syncFloat.value.finished)) {
syncDialogRef.value?.open(null, false)
return
}
const q = buildFilterQuery()
const identifierCount = (q.orderIdentifiers || []).length
const hasIdentifier = identifierCount > 0
const hasCustomer = !!(q.customerKeywords && q.customerKeywords.length)
const hasTime = !!q.beginTime && !!q.endTime
// 与查询同口径:有单号即可同步;无单号时必须客户+提货时间
// Same as list query: identifiers alone OK; else customer + pickup time required.
if (!hasIdentifier && !(hasCustomer && hasTime)) {
ElMessage.warning('请输入单号,或选择客户和提货时间后再同步落库')
return
@@ -481,13 +486,19 @@ function openFilterSync() {
}).catch(() => {})
}
let syncFloatHideTimer = null
function onSyncProgress(payload) {
syncFloat.value = payload
if (syncFloatHideTimer) {
clearTimeout(syncFloatHideTimer)
syncFloatHideTimer = null
}
if (payload && payload.finished && !payload.loading) {
window.setTimeout(() => {
syncFloatHideTimer = window.setTimeout(() => {
if (syncFloat.value && syncFloat.value.finished && !syncFloat.value.loading) {
syncFloat.value = null
}
syncFloatHideTimer = null
}, 8000)
}
}