修复:加固批次管理异步状态

This commit is contained in:
焦龙言
2026-09-23 17:27:33 +08:00
parent 8221a5162d
commit a5cc91a92a
3 changed files with 215 additions and 34 deletions
@@ -2485,3 +2485,61 @@ test('测试批次管理与旧数据纳入遵守 root、权限和预览提交契
assert.doesNotMatch(source, /TestBatchAdoptionDialog/, `${reportPath} 不得提供纳入入口`)
}
})
test('测试批次授权、清理和对话框异步状态拒绝错误目标与过期会话', async () => {
const managementPath = 'src/views/system/testBatch/index.vue'
const management = await readFile(path.join(frontendRoot, managementPath), 'utf8')
const parseAssignedUserIds = Function(`return (${extractNamedFunction(management, 'parseAssignedUserIds')})`)()
const shouldLeaveTestMode = Function(`return (${extractNamedFunction(management, 'shouldLeaveTestMode')})`)()
assert.deepEqual(parseAssignedUserIds(''), { valid: true, userIds: [] }, '空输入是显式移除全部授权')
assert.deepEqual(parseAssignedUserIds('1, 2,1'), { valid: true, userIds: [1, 2] })
for (const invalidInput of ['1,abc,2', '1,,2', '1,', '0', '-1', '1.5']) {
assert.deepEqual(parseAssignedUserIds(invalidInput), { valid: false, userIds: [] }, `${invalidInput} 必须整体拒绝`)
}
assert.equal(shouldLeaveTestMode('12', 12), true, '仅相同批次清理后退出测试模式')
assert.equal(shouldLeaveTestMode('13', 12), false, '清理其他批次不得退出当前测试模式')
assert.equal(shouldLeaveTestMode('', 12), false)
assert.doesNotMatch(management, /row\.userCount/, '列表 VO 未声明 userCount,禁止直接消费')
assert.match(management, /let listRequestVersion = 0/)
assert.match(management, /function hydrateParticipantCounts\(/)
assert.match(management, /participantState:\s*['"]loading['"]/, '详情 hydration 前必须显式 loading')
assert.match(management, /participantState:\s*['"]error['"]/, '单个详情失败必须展示明确失败状态')
assert.match(management, /userIds\.length/, '参与人数必须来自详情已声明的 userIds')
assert.match(management, /function isCurrentListRequest\(listContext\)[\s\S]*?listContext\.version === listRequestVersion/)
assert.match(management, /const detailLoaded = ref\(false\)/)
assert.match(management, /let assignSession = 0/)
assert.match(management, /function isCurrentAssignContext\(context\)[\s\S]*?context\.session === assignSession[\s\S]*?context\.batchId === assignRequestBatchId\.value/)
const openAssignSource = extractNamedFunction(management, 'openAssign')
assert.match(openAssignSource, /detailLoaded\.value = false/)
assert.match(openAssignSource, /if \(!isCurrentAssignContext\(context\)\) return/)
const submitAssignSource = extractNamedFunction(management, 'submitAssign')
assert.match(submitAssignSource, /if \(assignSubmitting\.value \|\| !detailLoaded\.value \|\| !assignBatch\.value\) return/)
assert.match(submitAssignSource, /const parsed = parseAssignedUserIds\(assignedUserIds\.value\)/)
assert.match(submitAssignSource, /if \(!parsed\.valid\)/)
assert.match(submitAssignSource, /if \(!isCurrentAssignContext\(context\)\) return/)
assert.match(management, /function resetAssignDialog\(\)[\s\S]*?assignSession \+= 1[\s\S]*?assignBatch\.value = null[\s\S]*?detailLoaded\.value = false/)
assert.match(management, /:disabled="assignLoading \|\| !detailLoaded \|\| assignSubmitting"/)
assert.match(management, /if \(shouldLeaveTestMode\(testBatchStore\.currentBatchId, purgeBatch\.value\.batchId\)\) \{\s*testBatchStore\.leaveTestMode\(\)/)
assert.match(management, /class="test-batch-dialog"/)
assert.match(management, /:column="descriptionColumns"/)
assert.match(management, /max-height:\s*90vh/)
assert.match(management, /overflow-y:\s*auto/)
const adoptionPath = 'src/components/TestBatchAdoptionDialog/index.vue'
const adoption = await readFile(path.join(frontendRoot, adoptionPath), 'utf8')
assert.match(adoption, /let dialogSession = 0/)
assert.match(adoption, /let optionsRequestVersion = 0/)
assert.match(adoption, /function isCurrentOptionsRequest\(context\)[\s\S]*?context\.session === dialogSession[\s\S]*?context\.version === optionsRequestVersion[\s\S]*?visible\.value/)
const loadBatchesSource = extractNamedFunction(adoption, 'loadBatches')
for (const callback of ['then', 'catch', 'finally']) {
assert.match(loadBatchesSource, new RegExp(`\\.${callback}\\([\\s\\S]*?if \\(!isCurrentOptionsRequest\\(context\\)\\) return`), `options ${callback} 必须拒绝过期会话`)
}
assert.match(adoption, /function resetState\(\)[\s\S]*?dialogSession \+= 1[\s\S]*?optionsRequestVersion \+= 1/)
assert.match(adoption, /class="test-batch-adoption-dialog"/)
assert.match(adoption, /:column="descriptionColumns"/)
assert.match(adoption, /max-height:\s*90vh/)
assert.match(adoption, /overflow-y:\s*auto/)
})
@@ -1,6 +1,7 @@
<template>
<el-dialog
v-model="visible"
class="test-batch-adoption-dialog"
title="纳入测试批次"
width="min(820px, 94vw)"
top="5vh"
@@ -35,7 +36,7 @@
<el-skeleton v-if="previewLoading" :rows="5" animated />
<el-empty v-else-if="!preview && !errorMessage && !batchLoading" description="暂无可预览的范围" :image-size="72" />
<template v-else-if="preview">
<el-descriptions :column="2" border size="small" class="mb8">
<el-descriptions :column="descriptionColumns" border size="small" class="mb8">
<el-descriptions-item label="目标批次">{{ preview.batchName }}</el-descriptions-item>
<el-descriptions-item label="影响总数">{{ preview.totalCount }}</el-descriptions-item>
</el-descriptions>
@@ -84,6 +85,9 @@ const batchId = ref(undefined)
const preview = ref(null)
const previewToken = ref('')
const batchName = ref('')
const descriptionColumns = ref(2)
let dialogSession = 0
let optionsRequestVersion = 0
let requestVersion = 0
const roots = computed(() => [{
@@ -103,6 +107,8 @@ function invalidatePreview() {
}
function resetState() {
dialogSession += 1
optionsRequestVersion += 1
invalidatePreview()
batchLoading.value = false
submitting.value = false
@@ -111,6 +117,14 @@ function resetState() {
batchId.value = undefined
}
function updateDescriptionColumns() { descriptionColumns.value = window.innerWidth <= 767 ? 1 : 2 }
function isCurrentOptionsRequest(context) {
return context.session === dialogSession && context.version === optionsRequestVersion && visible.value
}
function isCurrentPreviewRequest(context) {
return context.session === dialogSession && context.version === requestVersion && context.batchId === batchId.value && visible.value
}
function close() {
if (!submitting.value) visible.value = false
}
@@ -134,22 +148,21 @@ function requestPreview() {
return
}
invalidatePreview()
const currentVersion = ++requestVersion
const targetBatchId = batchId.value
const context = { session: dialogSession, version: ++requestVersion, batchId: batchId.value }
previewLoading.value = true
errorMessage.value = ''
previewTestBatchAdoption(targetBatchId, { roots: roots.value }).then((response) => {
if (currentVersion !== requestVersion || targetBatchId !== batchId.value) return
previewTestBatchAdoption(context.batchId, { roots: roots.value }).then((response) => {
if (!isCurrentPreviewRequest(context)) return
const data = response.data || {}
if (!data.previewToken || !data.batchName) throw new Error('预览响应不完整')
preview.value = normalizePreview(data)
previewToken.value = data.previewToken
}).catch(() => {
if (currentVersion !== requestVersion || targetBatchId !== batchId.value) return
if (!isCurrentPreviewRequest(context)) return
invalidatePreview()
errorMessage.value = '预览已失效或目标批次不可用,请重新预览'
}).finally(() => {
if (currentVersion === requestVersion && targetBatchId === batchId.value) previewLoading.value = false
if (isCurrentPreviewRequest(context)) previewLoading.value = false
})
}
@@ -159,9 +172,11 @@ function handleBatchChange() {
}
function loadBatches() {
const context = { session: dialogSession, version: ++optionsRequestVersion }
batchLoading.value = true
errorMessage.value = ''
return listAvailableTestBatches().then((response) => {
if (!isCurrentOptionsRequest(context)) return
activeBatches.value = (Array.isArray(response.data) ? response.data : []).filter((item) => item.status === 'ACTIVE')
batchId.value = activeBatches.value[0]?.batchId
if (!batchId.value) {
@@ -170,10 +185,12 @@ function loadBatches() {
}
requestPreview()
}).catch(() => {
if (!isCurrentOptionsRequest(context)) return
activeBatches.value = []
batchId.value = undefined
errorMessage.value = '测试批次加载失败,请稍后重试'
}).finally(() => {
if (!isCurrentOptionsRequest(context)) return
batchLoading.value = false
})
}
@@ -185,6 +202,7 @@ function open() {
}
resetState()
visible.value = true
updateDescriptionColumns()
loadBatches()
}
@@ -211,10 +229,15 @@ watch(() => props.resourceIds, () => {
if (visible.value) invalidatePreview()
}, { deep: true })
onMounted(() => window.addEventListener('resize', updateDescriptionColumns))
onBeforeUnmount(() => window.removeEventListener('resize', updateDescriptionColumns))
defineExpose({ open })
</script>
<style scoped>
.impact-keys { display: block; overflow-wrap: anywhere; white-space: normal; }
.confirm-form { margin-top: 12px; }
:global(.test-batch-adoption-dialog) { display:flex; flex-direction:column; max-height:90vh; }
:global(.test-batch-adoption-dialog .el-dialog__body) { flex:1; min-height:0; overflow-y:auto; }
</style>
@@ -16,7 +16,11 @@
<template #default="scope"><el-tag :type="statusType(scope.row.status)">{{ scope.row.status }}</el-tag></template>
</el-table-column>
<el-table-column label="参与用户" min-width="100" align="center">
<template #default="scope">{{ scope.row.userCount ?? '-' }}</template>
<template #default="scope">
<span v-if="scope.row.participantState === 'ready'">{{ scope.row.participantCount }}</span>
<el-tag v-else-if="scope.row.participantState === 'loading'" type="info" size="small">加载中</el-tag>
<span v-else class="participant-error">加载失败</span>
</template>
</el-table-column>
<el-table-column label="创建时间" prop="createTime" width="180"><template #default="scope">{{ parseTime(scope.row.createTime) }}</template></el-table-column>
<el-table-column label="关闭时间" prop="closedAt" width="180"><template #default="scope">{{ parseTime(scope.row.closedAt) }}</template></el-table-column>
@@ -34,7 +38,7 @@
</el-table>
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
<el-dialog v-model="formVisible" :title="form.batchId ? '编辑测试批次' : '创建测试批次'" width="min(560px, 92vw)" append-to-body :close-on-click-modal="!formSubmitting" :close-on-press-escape="!formSubmitting" :show-close="!formSubmitting">
<el-dialog v-model="formVisible" class="test-batch-dialog" :title="form.batchId ? '编辑测试批次' : '创建测试批次'" width="min(560px, 92vw)" append-to-body :close-on-click-modal="!formSubmitting" :close-on-press-escape="!formSubmitting" :show-close="!formSubmitting">
<el-alert v-if="formError" :title="formError" type="error" :closable="false" show-icon class="mb8" />
<el-form ref="formRef" :model="form" :rules="formRules" label-width="90px">
<el-form-item label="批次名称" prop="batchName"><el-input v-model="form.batchName" :disabled="Boolean(form.batchId) || formSubmitting" maxlength="100" /></el-form-item>
@@ -43,21 +47,21 @@
<template #footer><el-button :disabled="formSubmitting" @click="formVisible = false">取消</el-button><el-button type="primary" :loading="formSubmitting" @click="submitForm">确定</el-button></template>
</el-dialog>
<el-dialog v-model="assignVisible" title="授权测试批次用户" width="min(620px, 92vw)" append-to-body :close-on-click-modal="!assignSubmitting" :close-on-press-escape="!assignSubmitting" :show-close="!assignSubmitting">
<el-dialog v-model="assignVisible" class="test-batch-dialog" title="授权测试批次用户" width="min(620px, 92vw)" append-to-body :close-on-click-modal="!assignSubmitting" :close-on-press-escape="!assignSubmitting" :show-close="!assignSubmitting" @closed="resetAssignDialog">
<el-alert v-if="assignError" :title="assignError" type="error" :closable="false" show-icon class="mb8" />
<el-skeleton v-if="assignLoading" :rows="3" animated />
<el-form v-else label-width="100px">
<el-form-item label="批次">{{ assignBatch?.batchName }}</el-form-item>
<el-form-item label="用户 ID"><el-input v-model="assignedUserIds" :disabled="assignSubmitting" placeholder="多个 ID 用英文逗号分隔;留空将取消全部授权" /></el-form-item>
</el-form>
<template #footer><el-button :disabled="assignSubmitting" @click="assignVisible = false">取消</el-button><el-button type="primary" :loading="assignSubmitting" :disabled="assignLoading" @click="submitAssign">保存授权</el-button></template>
<template #footer><el-button :disabled="assignSubmitting" @click="assignVisible = false">取消</el-button><el-button type="primary" :loading="assignSubmitting" :disabled="assignLoading || !detailLoaded || assignSubmitting" @click="submitAssign">保存授权</el-button></template>
</el-dialog>
<el-dialog v-model="purgeVisible" title="清理测试批次" width="min(860px, 94vw)" top="5vh" append-to-body :close-on-click-modal="!purgeSubmitting" :close-on-press-escape="!purgeSubmitting" :show-close="!purgeSubmitting">
<el-dialog v-model="purgeVisible" class="test-batch-dialog" title="清理测试批次" width="min(860px, 94vw)" top="5vh" append-to-body :close-on-click-modal="!purgeSubmitting" :close-on-press-escape="!purgeSubmitting" :show-close="!purgeSubmitting" @closed="resetPurgeDialog">
<el-alert v-if="purgeError" :title="purgeError" type="error" :closable="false" show-icon class="mb8" />
<el-skeleton v-if="purgePreviewLoading" :rows="5" animated />
<template v-else-if="purgePreview">
<el-descriptions :column="2" border size="small" class="mb8">
<el-descriptions :column="descriptionColumns" border size="small" class="mb8">
<el-descriptions-item label="批次">{{ purgePreview.batchName }}</el-descriptions-item>
<el-descriptions-item label="影响总数">{{ purgePreview.totalCount }}</el-descriptions-item>
<el-descriptions-item label="在线清理上限">{{ purgePreview.onlinePurgeLimit }}</el-descriptions-item>
@@ -75,8 +79,8 @@
<template #footer><el-button :disabled="purgeSubmitting" @click="purgeVisible = false">取消</el-button><el-button type="primary" :loading="purgePreviewLoading" :disabled="purgePreviewLoading || purgeSubmitting" @click="loadPurgePreview">重新预览</el-button><el-button type="danger" :loading="purgeSubmitting" :disabled="!canPurge" @click="submitPurge">确认清理</el-button></template>
</el-dialog>
<el-dialog v-model="auditVisible" title="清理审计汇总" width="min(620px, 92vw)" append-to-body>
<el-descriptions :column="1" border><el-descriptions-item label="批次">{{ auditBatch?.batchName }}</el-descriptions-item><el-descriptions-item label="清理时间">{{ parseTime(auditBatch?.purgedAt) }}</el-descriptions-item><el-descriptions-item label="清理汇总"><pre class="audit-summary">{{ JSON.stringify(auditBatch?.purgeSummary || {}, null, 2) }}</pre></el-descriptions-item></el-descriptions>
<el-dialog v-model="auditVisible" class="test-batch-dialog" title="清理审计汇总" width="min(620px, 92vw)" append-to-body>
<el-descriptions :column="descriptionColumns" border><el-descriptions-item label="批次">{{ auditBatch?.batchName }}</el-descriptions-item><el-descriptions-item label="清理时间">{{ parseTime(auditBatch?.purgedAt) }}</el-descriptions-item><el-descriptions-item label="清理汇总"><pre class="audit-summary">{{ JSON.stringify(auditBatch?.purgeSummary || {}, null, 2) }}</pre></el-descriptions-item></el-descriptions>
</el-dialog>
</div>
<el-empty v-else description="无测试批次管理权限" />
@@ -107,6 +111,8 @@ const assignSubmitting = ref(false)
const assignError = ref('')
const assignBatch = ref(null)
const assignedUserIds = ref('')
const detailLoaded = ref(false)
const assignRequestBatchId = ref(undefined)
const purgeVisible = ref(false)
const purgePreviewLoading = ref(false)
const purgeSubmitting = ref(false)
@@ -118,7 +124,11 @@ const purgeBatchName = ref('')
const onlinePurgeAllowed = ref(false)
const auditVisible = ref(false)
const auditBatch = ref(null)
const descriptionColumns = ref(2)
let listRequestVersion = 0
let assignSession = 0
let purgeVersion = 0
let purgeSession = 0
const queryParams = ref({ pageNum: 1, pageSize: 10 })
const formRules = { batchName: [{ required: true, message: '请输入批次名称', trigger: 'blur' }] }
const actionsFor = row => ({
@@ -133,17 +143,41 @@ const canPurge = computed(() => Boolean(purgePreview.value && purgeToken.value &
const isOnlinePurgeBlocked = computed(() => onlinePurgeAllowed.value === false)
function statusType(status) { return status === 'ACTIVE' ? 'success' : status === 'CLOSED' ? 'warning' : 'info' }
function updateDescriptionColumns() { descriptionColumns.value = window.innerWidth <= 767 ? 1 : 2 }
function isCurrentListRequest(listContext) { return listContext.version === listRequestVersion }
function updateParticipantRow(batchId, listContext, patch) {
if (!isCurrentListRequest(listContext)) return
batchList.value = batchList.value.map((row) => row.batchId === batchId ? { ...row, ...patch } : row)
}
function hydrateParticipantCounts(rows, listContext) {
rows.forEach((row) => {
const batchId = row.batchId
getTestBatch(batchId).then((response) => {
if (!isCurrentListRequest(listContext)) return
const userIds = response.data?.userIds
if (!Array.isArray(userIds)) throw new Error('批次详情未返回用户授权')
updateParticipantRow(batchId, listContext, { participantState: 'ready', participantCount: userIds.length })
}).catch(() => {
updateParticipantRow(batchId, listContext, { participantState: 'error', participantCount: undefined })
})
})
}
function getList() {
const listContext = { version: ++listRequestVersion }
loading.value = true
loadError.value = ''
return listTestBatches(queryParams.value).then((response) => {
batchList.value = Array.isArray(response.rows) ? response.rows : []
if (!isCurrentListRequest(listContext)) return
const rows = Array.isArray(response.rows) ? response.rows : []
batchList.value = rows.map((row) => ({ ...row, participantState: 'loading', participantCount: undefined }))
total.value = response.total || 0
hydrateParticipantCounts(rows, listContext)
}).catch(() => {
if (!isCurrentListRequest(listContext)) return
batchList.value = []
total.value = 0
loadError.value = '测试批次列表加载失败,请稍后重试'
}).finally(() => { loading.value = false })
}).finally(() => { if (isCurrentListRequest(listContext)) loading.value = false })
}
async function refreshAfterSuccess(message) { proxy.$modal.msgSuccess(message); await Promise.all([getList(), testBatchStore.refreshOptions()]) }
function openCreate() { Object.assign(form, { batchId: undefined, batchName: '', remark: '' }); formError.value = ''; formVisible.value = true }
@@ -170,41 +204,107 @@ function closeBatch(row) {
function reopenBatch(row) {
runOperation(`reopen:${row.batchId}`, () => reopenTestBatch(row.batchId).then((response) => refreshAfterSuccess(response.msg || '批次已重新开启')).catch(() => proxy.$modal.msgError('重新开启批次失败')))
}
function parseUserIds() { return [...new Set(assignedUserIds.value.split(',').map((value) => Number(value.trim())).filter((id) => Number.isInteger(id) && id > 0))] }
function parseAssignedUserIds(value) {
if (typeof value !== 'string' || !value.trim()) return { valid: true, userIds: [] }
const userIds = []
for (const token of value.split(',')) {
const normalized = token.trim()
if (!/^[1-9]\d*$/.test(normalized)) return { valid: false, userIds: [] }
const userId = Number(normalized)
if (!Number.isSafeInteger(userId)) return { valid: false, userIds: [] }
userIds.push(userId)
}
return { valid: true, userIds: [...new Set(userIds)] }
}
function shouldLeaveTestMode(currentBatchId, batchId) { return Boolean(currentBatchId) && Boolean(batchId) && String(currentBatchId) === String(batchId) }
function resetAssignDialog() {
assignSession += 1
assignLoading.value = false
assignSubmitting.value = false
assignError.value = ''
assignBatch.value = null
assignRequestBatchId.value = undefined
assignedUserIds.value = ''
detailLoaded.value = false
}
function isCurrentAssignContext(context) {
return context.session === assignSession && context.batchId === assignRequestBatchId.value && assignVisible.value
}
function openAssign(row) {
assignVisible.value = true; assignLoading.value = true; assignError.value = ''; assignBatch.value = row; assignedUserIds.value = ''
getTestBatch(row.batchId).then((response) => { const detail = response.data || {}; assignBatch.value = detail; assignedUserIds.value = (detail.userIds || []).join(', ') }).catch(() => { assignError.value = '批次详情加载失败,请稍后重试' }).finally(() => { assignLoading.value = false })
resetAssignDialog()
assignVisible.value = true
assignLoading.value = true
assignRequestBatchId.value = row.batchId
const context = { session: assignSession, batchId: row.batchId }
getTestBatch(context.batchId).then((response) => {
if (!isCurrentAssignContext(context)) return
const detail = response.data || {}
if (detail.batchId !== context.batchId || !Array.isArray(detail.userIds)) throw new Error('批次详情不完整')
assignBatch.value = detail
assignedUserIds.value = detail.userIds.join(', ')
detailLoaded.value = true
}).catch(() => {
if (!isCurrentAssignContext(context)) return
assignBatch.value = null
assignedUserIds.value = ''
detailLoaded.value = false
assignError.value = '批次详情加载失败,请关闭后重试'
}).finally(() => { if (isCurrentAssignContext(context)) assignLoading.value = false })
}
function submitAssign() {
if (assignSubmitting.value || !assignBatch.value) return
const userIds = parseUserIds()
const source = assignedUserIds.value.trim()
if (source && !userIds.length) { assignError.value = '用户 ID 必须是正整数,多个 ID 用英文逗号分隔'; return }
if (assignSubmitting.value || !detailLoaded.value || !assignBatch.value) return
const context = { session: assignSession, batchId: assignBatch.value.batchId }
if (!isCurrentAssignContext(context)) return
const parsed = parseAssignedUserIds(assignedUserIds.value)
if (!parsed.valid) { assignError.value = '用户 ID 必须是正整数,多个 ID 用英文逗号分隔且不能留空项'; return }
assignSubmitting.value = true; assignError.value = ''
assignTestBatchUsers(assignBatch.value.batchId, { userIds }).then(async (response) => { assignVisible.value = false; await refreshAfterSuccess(response.msg || '用户授权已更新') }).catch(() => { assignError.value = '用户授权失败,请稍后重试' }).finally(() => { assignSubmitting.value = false })
assignTestBatchUsers(context.batchId, { userIds: parsed.userIds }).then(async (response) => {
if (!isCurrentAssignContext(context)) return
assignVisible.value = false
await refreshAfterSuccess(response.msg || '用户授权已更新')
}).catch(() => {
if (!isCurrentAssignContext(context)) return
assignError.value = '用户授权失败,请稍后重试'
}).finally(() => { if (isCurrentAssignContext(context)) assignSubmitting.value = false })
}
function invalidatePurgePreview() { purgeVersion += 1; purgePreview.value = null; purgeToken.value = ''; purgeBatchName.value = ''; onlinePurgeAllowed.value = false }
function openPurge(row) { purgeBatch.value = row; purgeError.value = ''; purgeVisible.value = true; invalidatePurgePreview(); loadPurgePreview() }
function clearPurgePreview() { purgePreview.value = null; purgeToken.value = ''; purgeBatchName.value = ''; onlinePurgeAllowed.value = false }
function resetPurgeDialog() { purgeSession += 1; purgeVersion += 1; purgePreviewLoading.value = false; purgeSubmitting.value = false; purgeError.value = ''; purgeBatch.value = null; clearPurgePreview() }
function isCurrentPurgeContext(context) { return context.session === purgeSession && context.version === purgeVersion && context.batchId === purgeBatch.value?.batchId && purgeVisible.value }
function openPurge(row) { resetPurgeDialog(); purgeBatch.value = row; purgeError.value = ''; purgeVisible.value = true; loadPurgePreview() }
function loadPurgePreview() {
if (!purgeBatch.value || purgePreviewLoading.value || purgeSubmitting.value) return
invalidatePurgePreview(); const version = ++purgeVersion; purgePreviewLoading.value = true; purgeError.value = ''
clearPurgePreview()
const context = { session: purgeSession, version: ++purgeVersion, batchId: purgeBatch.value.batchId }
purgePreviewLoading.value = true; purgeError.value = ''
previewTestBatchPurge(purgeBatch.value.batchId).then((response) => {
if (version !== purgeVersion) return
if (!isCurrentPurgeContext(context)) return
const data = response.data || {}
if (!data.previewToken || !data.batchName) throw new Error('预览响应不完整')
purgePreview.value = data; purgeToken.value = data.previewToken; onlinePurgeAllowed.value = data.onlinePurgeAllowed === true
}).catch(() => { if (version === purgeVersion) { invalidatePurgePreview(); purgeError.value = '清理预览已失效或批次状态已变化,请重新预览' } }).finally(() => { if (version === purgeVersion) purgePreviewLoading.value = false })
}).catch(() => { if (isCurrentPurgeContext(context)) { clearPurgePreview(); purgeError.value = '清理预览已失效或批次状态已变化,请重新预览' } }).finally(() => { if (isCurrentPurgeContext(context)) purgePreviewLoading.value = false })
}
function submitPurge() {
if (!canPurge.value || purgeSubmitting.value || !purgeBatch.value) return
purgeSubmitting.value = true; purgeError.value = ''; const token = purgeToken.value
purgeTestBatch(purgeBatch.value.batchId, { batchName: purgeBatchName.value, previewToken: purgeToken.value }).then(async (response) => { purgeVisible.value = false; testBatchStore.leaveTestMode(); await refreshAfterSuccess(response.msg || '批次已清理') }).catch(() => { if (token === purgeToken.value) { invalidatePurgePreview(); purgeError.value = '清理预览令牌已失效,请重新预览' } }).finally(() => { purgeSubmitting.value = false })
const context = { session: purgeSession, version: purgeVersion, batchId: purgeBatch.value.batchId, token: purgeToken.value }
purgeSubmitting.value = true; purgeError.value = ''
purgeTestBatch(context.batchId, { batchName: purgeBatchName.value, previewToken: context.token }).then(async (response) => {
if (!isCurrentPurgeContext(context)) return
if (shouldLeaveTestMode(testBatchStore.currentBatchId, purgeBatch.value.batchId)) {
testBatchStore.leaveTestMode()
}
purgeVisible.value = false
await refreshAfterSuccess(response.msg || '批次已清理')
}).catch(() => { if (isCurrentPurgeContext(context) && context.token === purgeToken.value) { clearPurgePreview(); purgeError.value = '清理预览令牌已失效,请重新预览' } }).finally(() => { if (isCurrentPurgeContext(context)) purgeSubmitting.value = false })
}
function openAudit(row) { auditBatch.value = row; auditVisible.value = true }
onMounted(getList)
onMounted(() => { updateDescriptionColumns(); window.addEventListener('resize', updateDescriptionColumns); getList() })
onBeforeUnmount(() => window.removeEventListener('resize', updateDescriptionColumns))
</script>
<style scoped>
.page-toolbar { display:flex; align-items:center; justify-content:space-between; gap:16px; padding-bottom:12px; border-bottom:1px solid var(--el-border-color-lighter); }
.page-toolbar h2 { margin:0; font-size:18px; font-weight:600; }.page-toolbar p { margin:4px 0 0; color:var(--el-text-color-secondary); font-size:13px; }.impact-keys { display:block; overflow-wrap:anywhere; }.purge-confirm { margin-top:12px; }.audit-summary { margin:0; white-space:pre-wrap; overflow-wrap:anywhere; }@media (max-width:768px) { .page-toolbar { align-items:flex-start; flex-direction:column; }.page-toolbar :deep(.el-button) { width:100%; } }
.page-toolbar h2 { margin:0; font-size:18px; font-weight:600; }.page-toolbar p { margin:4px 0 0; color:var(--el-text-color-secondary); font-size:13px; }.impact-keys { display:block; overflow-wrap:anywhere; }.purge-confirm { margin-top:12px; }.audit-summary { margin:0; white-space:pre-wrap; overflow-wrap:anywhere; }.participant-error { color:var(--el-color-danger); font-size:12px; }
:global(.test-batch-dialog) { display:flex; flex-direction:column; max-height:90vh; }
:global(.test-batch-dialog .el-dialog__body) { flex:1; min-height:0; overflow-y:auto; }
@media (max-width:768px) { .page-toolbar { align-items:flex-start; flex-direction:column; }.page-toolbar :deep(.el-button) { width:100%; } }
</style>