增加进度条

This commit is contained in:
wuweihua
2026-11-04 19:38:13 +08:00
parent 942d5932ae
commit 557d43a0b8
8 changed files with 483 additions and 50 deletions
+29 -8
View File
@@ -102,23 +102,29 @@ export function getLocalRebillTask(taskId) {
})
}
export function cancelLocalRebill(taskId) {
return request({
url: '/price/billing/orderBill/rebill/task/' + taskId + '/cancel',
method: 'post'
})
}
/**
* Async local rebill; poll until done.
* @param {object} data LocalRebillRequest
* Poll local rebill task until terminal status.
* @param {string} taskId
* @param {(task: object) => void} [onProgress]
*/
export async function localRebillAsync(data, onProgress) {
const submitRes = await submitLocalRebill(data)
const d = submitRes && submitRes.data
const taskId = d && (d.taskId || d)
if (!taskId) throw new Error('missing local rebill taskId')
export async function pollLocalRebillTask(taskId, onProgress) {
const id = String(taskId || '')
if (!id) throw new Error('missing local rebill taskId')
const started = Date.now()
const maxMs = 30 * 60 * 1000
while (Date.now() - started < maxMs) {
const res = await getLocalRebillTask(String(taskId))
const res = await getLocalRebillTask(id)
const task = res.data || {}
if (typeof onProgress === 'function') onProgress(task)
if (task.status === 'SUCCESS') return { task }
if (task.status === 'CANCELLED') return { task, cancelled: true }
if (task.status === 'FAIL') {
const err = new Error(task.error || task.message || 'local rebill failed')
err.task = task
@@ -129,6 +135,21 @@ export async function localRebillAsync(data, onProgress) {
throw new Error('local rebill timeout')
}
/**
* Async local rebill; poll until done.
* @param {object} data LocalRebillRequest
* @param {(task: object) => void} [onProgress]
* @param {{ getTaskId?: (taskId: string) => void }} [options]
*/
export async function localRebillAsync(data, onProgress, options) {
const submitRes = await submitLocalRebill(data)
const d = submitRes && submitRes.data
const taskId = d && (d.taskId || d)
if (!taskId) throw new Error('missing local rebill taskId')
if (options && typeof options.getTaskId === 'function') options.getTaskId(String(taskId))
return pollLocalRebillTask(String(taskId), onProgress)
}
/** Receivable bill detail */
export function getBill(billId) {
return request({
+312 -40
View File
@@ -1,5 +1,19 @@
<template>
<div class="app-container order-bill-query">
<div v-if="rebillFloatVisible" class="retry-status-float">
<el-tooltip :content="rebillFloatTooltip" placement="bottom">
<el-button class="retry-status-button" type="primary" plain size="small" @click="openRebillProgress()">
<span class="retry-progress-ring" :style="{ '--retry-progress': rebillProgressDegree + 'deg' }" aria-hidden="true">
<span class="retry-progress-value">{{ rebillProgressText }}</span>
</span>
<span class="retry-status-text">
<strong>{{ rebillFloatTitle }}</strong>
<small>{{ rebillFloatSummary }}</small>
</span>
</el-button>
</el-tooltip>
</div>
<basic-table
ref="tableRef"
:columns="columns"
@@ -161,13 +175,12 @@
</basic-table>
<el-dialog
v-model="rebillProgressVisible"
title="本地重新计费"
:title="rebillDialogTitle"
width="480px"
append-to-body
destroy-on-close
:close-on-click-modal="!batchLoading"
:close-on-press-escape="!batchLoading"
:show-close="!batchLoading"
:close-on-click-modal="false"
:close-on-press-escape="true"
:show-close="true"
>
<el-progress
:percentage="rebillProgressPercent"
@@ -182,10 +195,17 @@
/ 成功 {{ rebillProgressSuccess }}
/ 失败 {{ rebillProgressFail }}
</div>
<p v-if="batchLoading" class="rebill-progress__hint">请勿关闭页面,大量订单重新计费可能需要一些时间</p>
<p v-if="batchLoading" class="rebill-progress__hint">计费在后台进行,可收起弹窗继续操作;请勿关闭或刷新本页</p>
<template #footer>
<el-button v-if="!batchLoading" type="primary" @click="rebillProgressVisible = false">关闭</el-button>
<el-button v-else disabled>执行中...</el-button>
<el-button v-if="batchLoading" @click="rebillProgressVisible = false">收起</el-button>
<el-button
v-if="batchLoading"
type="danger"
plain
:loading="rebillCancelLoading"
@click="handleCancelRebill"
>取消计费</el-button>
<el-button v-else type="primary" @click="rebillProgressVisible = false">关闭</el-button>
</template>
</el-dialog>
<order-source-sync-log-drawer ref="logDrawerRef" />
@@ -201,7 +221,7 @@
<script setup name="OrderBillQuery">
import { ElMessage, ElMessageBox } from 'element-plus'
import { listOrderBill, localRebillAsync } from '@/api/price/billing'
import { listOrderBill, localRebillAsync, pollLocalRebillTask, cancelLocalRebill, getLocalRebillTask } from '@/api/price/billing'
import { exportAsyncFromTable } from '@/utils/excelExport'
import ExcelExportFieldDialog from '@/components/ExcelExportFieldDialog/index.vue'
import { ORDER_BILL_EXPORT_FIELDS } from '@/utils/exportFieldDefs'
@@ -214,6 +234,7 @@ const logDrawerRef = ref()
const productCheckDrawerRef = ref()
const checkLoading = ref(false)
const batchLoading = ref(false)
const rebillFinished = ref(false)
const rebillProgressVisible = ref(false)
const rebillProgressPercent = ref(0)
const rebillProgressMessage = ref('')
@@ -221,6 +242,69 @@ const rebillProgressStatus = ref(undefined)
const rebillProgressTotal = ref(0)
const rebillProgressSuccess = ref(0)
const rebillProgressFail = ref(0)
const rebillTaskId = ref('')
const rebillCancelLoading = ref(false)
let rebillFloatHideTimer = null
const REBILL_TASK_STORAGE_KEY = 'price:orderBill:localRebillTaskId'
function saveRebillTaskId(taskId) {
rebillTaskId.value = taskId ? String(taskId) : ''
try {
if (rebillTaskId.value) sessionStorage.setItem(REBILL_TASK_STORAGE_KEY, rebillTaskId.value)
else sessionStorage.removeItem(REBILL_TASK_STORAGE_KEY)
} catch (e) { /* ignore */ }
}
function clearRebillTaskId() {
saveRebillTaskId('')
}
function readStoredRebillTaskId() {
try {
return String(sessionStorage.getItem(REBILL_TASK_STORAGE_KEY) || '').trim()
} catch (e) {
return ''
}
}
const rebillFloatVisible = computed(() => batchLoading.value || rebillFinished.value)
const rebillProgressDegree = computed(() => Math.round(((rebillProgressPercent.value || 0) / 100) * 360))
const rebillProgressText = computed(() => (rebillProgressPercent.value || 0) + '%')
const rebillDialogTitle = computed(() => {
if (batchLoading.value) return '异步重新计费进行中'
if (rebillFinished.value) return '异步重新计费完成'
return '本地重新计费'
})
const rebillFloatTitle = computed(() => {
if (batchLoading.value) return '异步重新计费中'
if (rebillProgressStatus.value === 'exception') return '重新计费失败'
if (rebillProgressStatus.value === 'warning' && String(rebillProgressMessage.value || '').includes('已取消')) return '重新计费已取消'
if (rebillFinished.value) return '重新计费完成'
return '异步重新计费'
})
const rebillFloatSummary = computed(() =>
'成功 ' + (rebillProgressSuccess.value || 0) +
' / 失败 ' + (rebillProgressFail.value || 0) +
' / 总数 ' + (rebillProgressTotal.value || 0)
)
const rebillFloatTooltip = computed(() => rebillProgressMessage.value || '点击查看详情')
function openRebillProgress() {
rebillProgressVisible.value = true
}
function scheduleHideRebillFloat() {
if (rebillFloatHideTimer) {
clearTimeout(rebillFloatHideTimer)
rebillFloatHideTimer = null
}
rebillFloatHideTimer = window.setTimeout(() => {
if (!batchLoading.value && rebillFinished.value) {
rebillFinished.value = false
}
rebillFloatHideTimer = null
}, 8000)
}
const tableHeight = ref(480)
let tableResizeObserver = null
const MAX_ORDER_IDENTIFIERS = 1000
@@ -504,49 +588,64 @@ function handleCheckProducts() {
}
}
async function runLocalRebill(payload, confirmText) {
try {
await ElMessageBox.confirm(confirmText, '二次确认', { type: 'warning' })
} catch (e) {
return
function applyRebillProgress(t) {
const total = Number(t.total) || 0
const success = Number(t.success) || 0
const fail = Number(t.fail) || 0
rebillProgressPercent.value = Math.min(100, Number(t.percent) || 0)
rebillProgressTotal.value = total
rebillProgressSuccess.value = success
rebillProgressFail.value = fail
if (t.status === 'PENDING') {
rebillProgressMessage.value = '排队中…'
} else if (t.status === 'RUNNING' && String(t.message || '').startsWith('cancelling')) {
rebillProgressMessage.value = '正在取消,等待当前批次结束… ' + success + '/' + total
} else if (t.status === 'RUNNING') {
rebillProgressMessage.value = '正在重新计费 ' + success + '/' + total +
(fail ? ('(失败 ' + fail + ')') : '')
}
}
function finishRebillFromTask(task, cancelled) {
const success = Number(task && task.success) || 0
const fail = Number(task && task.fail) || 0
rebillProgressPercent.value = Math.min(100, Number(task && task.percent) || 100)
rebillProgressStatus.value = cancelled ? 'warning' : (fail > 0 ? 'warning' : 'success')
rebillProgressTotal.value = Number(task && task.total) || rebillProgressTotal.value
rebillProgressSuccess.value = success
rebillProgressFail.value = fail
rebillProgressMessage.value = cancelled
? ('已取消:成功 ' + success + ' / 失败 ' + fail)
: ('完成:成功 ' + success + ' / 失败 ' + fail)
rebillFinished.value = true
clearRebillTaskId()
tableRef.value?.refresh?.()
if (cancelled) ElMessage.warning(rebillProgressMessage.value)
else ElMessage.success(rebillProgressMessage.value)
scheduleHideRebillFloat()
}
async function resumeRebillTask(taskId) {
if (!taskId || batchLoading.value) return
batchLoading.value = true
rebillProgressVisible.value = true
rebillFinished.value = false
rebillCancelLoading.value = false
saveRebillTaskId(taskId)
rebillProgressVisible.value = false
rebillProgressPercent.value = 0
rebillProgressStatus.value = undefined
rebillProgressMessage.value = '任务提交中…'
rebillProgressMessage.value = '恢复进度中…'
rebillProgressTotal.value = 0
rebillProgressSuccess.value = 0
rebillProgressFail.value = 0
try {
const { task } = await localRebillAsync(payload, (t) => {
const total = Number(t.total) || 0
const success = Number(t.success) || 0
const fail = Number(t.fail) || 0
rebillProgressPercent.value = Math.min(100, Number(t.percent) || 0)
rebillProgressTotal.value = total
rebillProgressSuccess.value = success
rebillProgressFail.value = fail
if (t.status === 'PENDING') {
rebillProgressMessage.value = '排队中…'
} else if (t.status === 'RUNNING') {
rebillProgressMessage.value = '正在重新计费 ' + success + '/' + total +
(fail ? ('(失败 ' + fail + ')') : '')
}
})
const success = Number(task && task.success) || 0
const fail = Number(task && task.fail) || 0
rebillProgressPercent.value = 100
rebillProgressStatus.value = fail > 0 ? 'warning' : 'success'
rebillProgressTotal.value = Number(task && task.total) || rebillProgressTotal.value
rebillProgressSuccess.value = success
rebillProgressFail.value = fail
rebillProgressMessage.value = '完成:成功 ' + success + ' / 失败 ' + fail
tableRef.value?.refresh?.()
const result = await pollLocalRebillTask(taskId, applyRebillProgress)
finishRebillFromTask(result && result.task, !!(result && result.cancelled))
} catch (e) {
const task = e && e.task
rebillProgressStatus.value = 'exception'
rebillProgressMessage.value = (e && e.message) || '重新计费失败'
rebillFinished.value = true
if (task) {
rebillProgressPercent.value = Math.min(100, Number(task.percent) || 100)
rebillProgressTotal.value = Number(task.total) || 0
@@ -554,11 +653,119 @@ async function runLocalRebill(payload, confirmText) {
rebillProgressFail.value = Number(task.fail) || 0
tableRef.value?.refresh?.()
}
clearRebillTaskId()
rebillProgressVisible.value = true
scheduleHideRebillFloat()
} finally {
batchLoading.value = false
}
}
async function tryRestoreRebillProgress() {
const taskId = readStoredRebillTaskId()
if (!taskId) return
try {
const res = await getLocalRebillTask(taskId)
const task = (res && res.data) || {}
const st = String(task.status || '').toUpperCase()
if (st === 'PENDING' || st === 'RUNNING') {
applyRebillProgress(task)
resumeRebillTask(taskId)
return
}
if (st === 'SUCCESS' || st === 'CANCELLED' || st === 'FAIL') {
clearRebillTaskId()
return
}
clearRebillTaskId()
} catch (e) {
clearRebillTaskId()
}
}
async function runLocalRebill(payload, confirmText) {
if (batchLoading.value) {
openRebillProgress()
return
}
try {
await ElMessageBox.confirm(confirmText, '二次确认', { type: 'warning' })
} catch (e) {
return
}
if (rebillFloatHideTimer) {
clearTimeout(rebillFloatHideTimer)
rebillFloatHideTimer = null
}
batchLoading.value = true
rebillFinished.value = false
rebillCancelLoading.value = false
clearRebillTaskId()
rebillProgressVisible.value = true
rebillProgressPercent.value = 0
rebillProgressStatus.value = undefined
rebillProgressMessage.value = '任务提交中…'
rebillProgressTotal.value = 0
rebillProgressSuccess.value = 0
rebillProgressFail.value = 0
window.setTimeout(() => {
if (batchLoading.value) rebillProgressVisible.value = false
}, 600)
try {
const result = await localRebillAsync(
payload,
applyRebillProgress,
{ getTaskId: (id) => { saveRebillTaskId(id) } }
)
finishRebillFromTask(result && result.task, !!(result && result.cancelled))
} catch (e) {
const task = e && e.task
rebillProgressStatus.value = 'exception'
rebillProgressMessage.value = (e && e.message) || '重新计费失败'
rebillFinished.value = true
if (task) {
rebillProgressPercent.value = Math.min(100, Number(task.percent) || 100)
rebillProgressTotal.value = Number(task.total) || 0
rebillProgressSuccess.value = Number(task.success) || 0
rebillProgressFail.value = Number(task.fail) || 0
tableRef.value?.refresh?.()
}
clearRebillTaskId()
rebillProgressVisible.value = true
scheduleHideRebillFloat()
} finally {
batchLoading.value = false
}
}
async function handleCancelRebill() {
if (!batchLoading.value || !rebillTaskId.value || rebillCancelLoading.value) return
try {
await ElMessageBox.confirm(
'确认取消当前重新计费?已计费完成的订单会保留,未开始的将停止。',
'取消计费',
{ type: 'warning' }
)
} catch (e) {
return
}
rebillCancelLoading.value = true
try {
const res = await cancelLocalRebill(rebillTaskId.value)
const task = (res && res.data) || {}
rebillProgressPercent.value = Math.min(100, Number(task.percent) || rebillProgressPercent.value || 0)
rebillProgressTotal.value = Number(task.total) || rebillProgressTotal.value
rebillProgressSuccess.value = Number(task.success) || rebillProgressSuccess.value
rebillProgressFail.value = Number(task.fail) || rebillProgressFail.value
rebillProgressMessage.value = '取消请求已提交,等待当前批次结束…'
ElMessage.success('已提交取消')
} catch (e) {
// global interceptor shows error
} finally {
rebillCancelLoading.value = false
}
}
function openFilterRebill() {
const filter = buildRebillFilter()
const identifiers = filter.orderIdentifiers || []
@@ -645,6 +852,7 @@ onMounted(() => {
}
})
window.addEventListener('resize', updateTableHeight)
tryRestoreRebillProgress()
})
onBeforeUnmount(() => {
@@ -653,6 +861,10 @@ onBeforeUnmount(() => {
tableResizeObserver.disconnect()
tableResizeObserver = null
}
if (rebillFloatHideTimer) {
clearTimeout(rebillFloatHideTimer)
rebillFloatHideTimer = null
}
})
</script>
@@ -666,4 +878,64 @@ onBeforeUnmount(() => {
.rebill-progress__msg { margin-top: 12px; color: var(--el-text-color-regular); }
.rebill-progress__text { margin-top: 8px; font-size: 13px; color: var(--el-text-color-secondary); }
.rebill-progress__hint { margin-top: 12px; font-size: 12px; color: var(--el-color-warning); }
.order-bill-query .retry-status-float {
position: fixed;
top: 92px;
right: 24px;
z-index: 3001;
}
.order-bill-query .retry-status-button {
display: inline-flex;
align-items: center;
gap: 10px;
min-width: 200px;
min-height: 68px;
padding: 6px 16px 6px 7px;
border-radius: 36px;
}
.order-bill-query .retry-progress-ring {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
flex: 0 0 54px;
width: 54px;
height: 54px;
border-radius: 50%;
background: conic-gradient(
var(--el-color-primary) 0deg var(--retry-progress, 45deg),
var(--el-color-primary-light-8) var(--retry-progress, 45deg) 360deg
);
}
.order-bill-query .retry-progress-ring::after {
position: absolute;
inset: 5px;
content: '';
border-radius: 50%;
background: var(--el-bg-color);
}
.order-bill-query .retry-progress-value {
position: relative;
z-index: 1;
color: var(--el-color-primary);
font-size: 12px;
font-weight: 600;
line-height: 1;
}
.order-bill-query .retry-status-text {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 3px;
line-height: 1.2;
white-space: nowrap;
}
.order-bill-query .retry-status-text strong {
font-size: 14px;
font-weight: 600;
}
.order-bill-query .retry-status-text small {
color: var(--el-text-color-secondary);
font-size: 11px;
}
</style>