diff --git a/bms-front/src/views/monitor/orderSource/components/OrderSourceSyncDialog.vue b/bms-front/src/views/monitor/orderSource/components/OrderSourceSyncDialog.vue index d9d32455..47455435 100644 --- a/bms-front/src/views/monitor/orderSource/components/OrderSourceSyncDialog.vue +++ b/bms-front/src/views/monitor/orderSource/components/OrderSourceSyncDialog.vue @@ -2,12 +2,11 @@ @@ -28,7 +27,7 @@ /> -
+
-
{{ progressMessage }}
-
+
{{ progressMessage }}
+
总数 {{ progress.total || 0 }} - / 已处理 {{ progress.processed || 0 }} / 成功 {{ progress.ok || 0 }} / 失败 {{ progress.fail || 0 }}
-
{{ progress.error }}
-

请勿关闭页面,大量订单同步落库可能需要一些时间

+
{{ progress.error }}
+

同步在后台进行,可收起弹窗继续操作;请勿关闭或刷新本页

- @@ -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 }) diff --git a/bms-front/src/views/monitor/orderSource/index.vue b/bms-front/src/views/monitor/orderSource/index.vue index 7f57f688..f853b21f 100644 --- a/bms-front/src/views/monitor/orderSource/index.vue +++ b/bms-front/src/views/monitor/orderSource/index.vue @@ -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) } }