优化订单数源的进度条
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user