后台继续生成符合“取消需二次确认”的现有逻辑,但弹窗状态没有跟随任务完成状态更新,属于交互竞态问题 已修复
This commit is contained in:
@@ -3,7 +3,7 @@
|
|||||||
<div v-if="generateStatusVisible" class="generate-status-float">
|
<div v-if="generateStatusVisible" class="generate-status-float">
|
||||||
<el-tooltip :content="generateStatusTooltip" placement="bottom">
|
<el-tooltip :content="generateStatusTooltip" placement="bottom">
|
||||||
<el-button class="generate-status-button" type="primary" plain size="small"
|
<el-button class="generate-status-button" type="primary" plain size="small"
|
||||||
:disabled="activeGenerateTask?.status === 'CANCEL_REQUESTED'" @click="cancelActiveGenerateTask">
|
:disabled="!canCancelActiveGenerateTask" @click="cancelActiveGenerateTask">
|
||||||
<span class="generate-progress-ring" :style="{ '--generate-progress': `${generateProgressDegree}deg` }" aria-hidden="true">
|
<span class="generate-progress-ring" :style="{ '--generate-progress': `${generateProgressDegree}deg` }" aria-hidden="true">
|
||||||
<span class="generate-progress-value">{{ generateProgressText }}</span>
|
<span class="generate-progress-value">{{ generateProgressText }}</span>
|
||||||
</span>
|
</span>
|
||||||
@@ -50,6 +50,16 @@
|
|||||||
|
|
||||||
<bill-detail v-model="detailOpen" :detail="detail" :loading="detailLoading" @download="downloadExport" @export="exportBill(detail)" />
|
<bill-detail v-model="detailOpen" :detail="detail" :loading="detailLoading" @download="downloadExport" @export="exportBill(detail)" />
|
||||||
<generate-bill-dialog v-model="generateOpen" @success="handleGenerateSuccess" />
|
<generate-bill-dialog v-model="generateOpen" @success="handleGenerateSuccess" />
|
||||||
|
<el-dialog v-model="cancelConfirmVisible" title="取消账单生成" width="420px" append-to-body
|
||||||
|
:close-on-click-modal="false" :close-on-press-escape="!cancelConfirmLoading"
|
||||||
|
:show-close="!cancelConfirmLoading" @closed="handleCancelConfirmClosed">
|
||||||
|
<p class="cancel-generate-message">账单正在后台生成,确定取消当前任务吗?</p>
|
||||||
|
<template #footer>
|
||||||
|
<el-button :disabled="cancelConfirmLoading" @click="cancelConfirmVisible = false">暂不取消</el-button>
|
||||||
|
<el-button type="warning" :loading="cancelConfirmLoading" :disabled="cancelConfirmDisabled"
|
||||||
|
@click="confirmCancelActiveGenerateTask">{{ cancelConfirmLoading ? '取消中' : '确定取消' }}</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -83,13 +93,27 @@ const generateTasks = reactive(new Map())
|
|||||||
const activeGenerateTasks = computed(() => Array.from(generateTasks.values()))
|
const activeGenerateTasks = computed(() => Array.from(generateTasks.values()))
|
||||||
const activeGenerateTask = computed(() => activeGenerateTasks.value[0])
|
const activeGenerateTask = computed(() => activeGenerateTasks.value[0])
|
||||||
const generateStatusVisible = computed(() => activeGenerateTasks.value.length > 0)
|
const generateStatusVisible = computed(() => activeGenerateTasks.value.length > 0)
|
||||||
|
const cancelConfirmVisible = ref(false)
|
||||||
|
const cancelConfirmLoading = ref(false)
|
||||||
|
const cancelConfirmTaskId = ref()
|
||||||
|
const cancelConfirmTask = computed(() => cancelConfirmTaskId.value ? generateTasks.get(cancelConfirmTaskId.value) : null)
|
||||||
|
const canCancelActiveGenerateTask = computed(() => ['WAITING', 'RUNNING'].includes(activeGenerateTask.value?.status))
|
||||||
|
const cancelConfirmDisabled = computed(() => {
|
||||||
|
return cancelConfirmLoading.value || !['WAITING', 'RUNNING'].includes(cancelConfirmTask.value?.status)
|
||||||
|
})
|
||||||
const generateStatusTitle = computed(() => {
|
const generateStatusTitle = computed(() => {
|
||||||
const status = activeGenerateTask.value?.status
|
const status = activeGenerateTask.value?.status
|
||||||
if (status === 'WAITING') return '账单排队中'
|
if (status === 'WAITING') return '账单排队中'
|
||||||
|
if (status === 'SUCCESS') return '账单生成完成'
|
||||||
|
if (status === 'FAILED') return '账单生成失败'
|
||||||
|
if (status === 'CANCELED') return '账单已取消'
|
||||||
if (status === 'CANCEL_REQUESTED') return '账单取消中'
|
if (status === 'CANCEL_REQUESTED') return '账单取消中'
|
||||||
return '账单生成中'
|
return '账单生成中'
|
||||||
})
|
})
|
||||||
const generateStatusTooltip = computed(() => {
|
const generateStatusTooltip = computed(() => {
|
||||||
|
if (['SUCCESS', 'FAILED', 'CANCELED'].includes(activeGenerateTask.value?.status)) {
|
||||||
|
return '任务状态已更新'
|
||||||
|
}
|
||||||
return activeGenerateTask.value?.status === 'CANCEL_REQUESTED'
|
return activeGenerateTask.value?.status === 'CANCEL_REQUESTED'
|
||||||
? '正在清理已生成的账单数据,请稍候'
|
? '正在清理已生成的账单数据,请稍候'
|
||||||
: '点击取消当前账单生成任务'
|
: '点击取消当前账单生成任务'
|
||||||
@@ -107,6 +131,8 @@ const generateProgressDegree = computed(() => {
|
|||||||
const generateStatusSummary = computed(() => {
|
const generateStatusSummary = computed(() => {
|
||||||
const task = activeGenerateTask.value
|
const task = activeGenerateTask.value
|
||||||
if (!task) return '任务提交中'
|
if (!task) return '任务提交中'
|
||||||
|
if (task.status === 'FAILED') return '任务执行失败'
|
||||||
|
if (task.status === 'CANCELED') return '任务已取消'
|
||||||
if (task.status === 'CANCEL_REQUESTED') return '正在清理数据'
|
if (task.status === 'CANCEL_REQUESTED') return '正在清理数据'
|
||||||
if (task.status === 'WAITING') return '等待后台处理'
|
if (task.status === 'WAITING') return '等待后台处理'
|
||||||
if (Number(task.totalCount) > 0) return `${Number(task.processedCount || 0)} / ${Number(task.totalCount)} 条`
|
if (Number(task.totalCount) > 0) return `${Number(task.processedCount || 0)} / ${Number(task.totalCount)} 条`
|
||||||
@@ -164,6 +190,7 @@ function startGeneratePolling(taskId) {
|
|||||||
|
|
||||||
function finishGenerateTask(taskId, task) {
|
function finishGenerateTask(taskId, task) {
|
||||||
stopGeneratePolling(taskId)
|
stopGeneratePolling(taskId)
|
||||||
|
closeCancelConfirm(taskId)
|
||||||
if (generateFinishTimers.has(taskId)) return
|
if (generateFinishTimers.has(taskId)) return
|
||||||
// 即使后台很快返回 SUCCESS,也延迟清理浮层,保证用户能看到结果反馈。
|
// 即使后台很快返回 SUCCESS,也延迟清理浮层,保证用户能看到结果反馈。
|
||||||
const visibleSince = generateVisibleSince.get(taskId) || Date.now()
|
const visibleSince = generateVisibleSince.get(taskId) || Date.now()
|
||||||
@@ -200,12 +227,50 @@ function stopGeneratePolling(taskId) {
|
|||||||
|
|
||||||
function cancelActiveGenerateTask() {
|
function cancelActiveGenerateTask() {
|
||||||
const task = activeGenerateTask.value
|
const task = activeGenerateTask.value
|
||||||
if (!task?.taskId) return
|
if (!task?.taskId || !canCancelActiveGenerateTask.value) return
|
||||||
proxy.$modal.confirm('账单正在后台生成,确定取消当前任务吗?').then(() => cancelArBillGenerateTask(task.taskId)).then(res => {
|
cancelConfirmTaskId.value = task.taskId
|
||||||
|
cancelConfirmLoading.value = false
|
||||||
|
cancelConfirmVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmCancelActiveGenerateTask() {
|
||||||
|
const taskId = cancelConfirmTaskId.value
|
||||||
|
const task = taskId ? generateTasks.get(taskId) : null
|
||||||
|
if (!taskId || !['WAITING', 'RUNNING'].includes(task?.status)) {
|
||||||
|
cancelConfirmVisible.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cancelConfirmLoading.value = true
|
||||||
|
try {
|
||||||
|
const res = await cancelArBillGenerateTask(taskId)
|
||||||
const updated = res.data
|
const updated = res.data
|
||||||
if (updated) generateTasks.set(updated.taskId, updated)
|
let cancellationSubmitted = true
|
||||||
proxy.$modal.msgSuccess('已提交取消请求')
|
if (updated?.taskId) {
|
||||||
}).catch(() => {})
|
generateTasks.set(updated.taskId, updated)
|
||||||
|
if (['SUCCESS', 'FAILED', 'CANCELED'].includes(updated.status)) {
|
||||||
|
finishGenerateTask(updated.taskId, updated)
|
||||||
|
cancellationSubmitted = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cancelConfirmVisible.value = false
|
||||||
|
if (cancellationSubmitted) proxy.$modal.msgSuccess('已提交取消请求')
|
||||||
|
} catch (error) {
|
||||||
|
// 请求失败时保留弹窗,允许用户重新确认;全局请求拦截器负责展示错误信息。
|
||||||
|
} finally {
|
||||||
|
cancelConfirmLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeCancelConfirm(taskId) {
|
||||||
|
if (cancelConfirmTaskId.value !== taskId) return
|
||||||
|
cancelConfirmVisible.value = false
|
||||||
|
cancelConfirmLoading.value = false
|
||||||
|
cancelConfirmTaskId.value = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCancelConfirmClosed() {
|
||||||
|
cancelConfirmLoading.value = false
|
||||||
|
cancelConfirmTaskId.value = undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadActiveGenerateTasks() {
|
function loadActiveGenerateTasks() {
|
||||||
@@ -390,6 +455,11 @@ function statusType(value) { return AR_BILL_STATUS_TYPES[value] || 'info' }
|
|||||||
.ar-bill-page .amount { color: #f56c6c; font-weight: 600; }
|
.ar-bill-page .amount { color: #f56c6c; font-weight: 600; }
|
||||||
.ar-bill-page .written { color: #67c23a; }
|
.ar-bill-page .written { color: #67c23a; }
|
||||||
.ar-bill-page .unwritten { color: #e6a23c; }
|
.ar-bill-page .unwritten { color: #e6a23c; }
|
||||||
|
.ar-bill-page .cancel-generate-message {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
.ar-bill-page .generate-status-float {
|
.ar-bill-page .generate-status-float {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
|||||||
Reference in New Issue
Block a user