修复:加固批次管理异步状态
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user