前端:增加批次管理与旧数据纳入

This commit is contained in:
焦龙言
2026-09-23 16:54:22 +08:00
parent a7dc1a2a1e
commit 8221a5162d
15 changed files with 745 additions and 17 deletions
@@ -2414,3 +2414,74 @@ test('业务用户分配组件使用固定映射并允许空数组完整替换',
assert.match(source, /<el-alert\b[^>]*v-if="loadError"/)
assert.match(source, /width="min\(720px, 92vw\)"/)
})
test('测试批次管理与旧数据纳入遵守 root、权限和预览提交契约', async () => {
const apiPath = 'src/api/system/testBatch.js'
const apiSource = await readFile(path.join(frontendRoot, apiPath), 'utf8')
const expectedTestBatchRequests = [
{ url: "'/system/test-batch/options'", method: 'get' },
{ url: "'/system/test-batch/list'", method: 'get' },
{ url: "'/system/test-batch/' + batchId", method: 'get' },
{ url: "'/system/test-batch'", method: 'post' },
{ url: "'/system/test-batch/' + batchId", method: 'put' },
{ url: "'/system/test-batch/' + batchId + '/users'", method: 'put' },
{ url: "'/system/test-batch/' + batchId + '/close'", method: 'put' },
{ url: "'/system/test-batch/' + batchId + '/reopen'", method: 'put' },
{ url: "'/system/test-batch/' + batchId + '/adoption/preview'", method: 'post' },
{ url: "'/system/test-batch/' + batchId + '/adoption/execute'", method: 'post' },
{ url: "'/system/test-batch/' + batchId + '/purge/preview'", method: 'post' },
{ url: "'/system/test-batch/' + batchId + '/purge'", method: 'post' }
]
assert.deepEqual(extractRequests(apiSource), expectedTestBatchRequests)
const adoptionRoots = {
strain: { path: 'src/views/master/strain/index.vue', id: 'strainId' },
customer: { path: 'src/views/master/customer/index.vue', id: 'customerId' },
site: { path: 'src/views/master/site/index.vue', id: 'siteId' },
line: { path: 'src/views/master/line/index.vue', id: 'lineId' },
warehouse: { path: 'src/views/master/warehouse/index.vue', id: 'warehouseId' },
pallet: { path: 'src/views/master/pallet/index.vue', id: 'palletId' },
inbound: { path: 'src/views/business/inbound/index.vue', id: 'inboundId' },
filling: { path: 'src/views/business/filling/index.vue', id: 'fillingId' },
shipment: { path: 'src/views/business/shipment/index.vue', id: 'shipmentId' },
usage: { path: 'src/views/business/usage/index.vue', id: 'usageId' },
return: { path: 'src/views/business/return/index.vue', id: 'returnId' }
}
assert.equal(Object.keys(adoptionRoots).length, 11, '纳入来源必须固定为 11 类')
for (const [rootType, contract] of Object.entries(adoptionRoots)) {
const source = await readFile(path.join(frontendRoot, contract.path), 'utf8')
assert.match(source, /import TestBatchAdoptionDialog from ['"]@\/components\/TestBatchAdoptionDialog['"]/)
assert.match(source, /useUserStore\(\)/, `${rootType} 必须读取 admin 可见性`)
assert.match(source, /useTestBatchStore\(\)/, `${rootType} 必须读取测试模式`)
assert.match(source, /userStore\.admin === true\s*&&\s*!testBatchStore\.isTestMode/, `${rootType} 纳入入口必须只在管理员正式模式显示`)
assert.match(source, new RegExp(`<test-batch-adoption-dialog\\b[\\s\\S]*?:root-type="['\"]${rootType}['\"][\\s\\S]*?:resource-ids="selectedIds"`))
assert.match(source, new RegExp(`selectedIds\\.value\\s*=\\s*selection\\.map\\(\\(item\\)\\s*=>\\s*item\\.${contract.id}\\)`), `${rootType} 必须传递正整数根 ID`)
}
for (const rootType of ['strain', 'customer', 'site', 'line', 'warehouse', 'pallet']) {
const source = await readFile(path.join(frontendRoot, adoptionRoots[rootType].path), 'utf8')
assert.match(source, /userStore\.admin === true\s*\|\|\s*!testBatchStore\.isTestMode/, `${rootType} 测试模式主数据写操作必须要求管理员`)
}
const adoptionDialogPath = 'src/components/TestBatchAdoptionDialog/index.vue'
const adoptionDialog = await readFile(path.join(frontendRoot, adoptionDialogPath), 'utf8')
assert.match(adoptionDialog, /item\.status === ['"]ACTIVE['"]/)
assert.match(adoptionDialog, /Number\.isInteger\(id\)\s*&&\s*id\s*>\s*0/)
assert.match(adoptionDialog, /previewTestBatchAdoption\(.*\{\s*roots:\s*roots\.value\s*}\s*\)/s)
assert.match(adoptionDialog, /executeTestBatchAdoption\([^,]+,\s*\{\s*roots:\s*roots\.value,\s*batchName:\s*batchName\.value,\s*previewToken:\s*previewToken\.value\s*}\s*\)/s)
assert.match(adoptionDialog, /batchName\.value\s*!==\s*preview\.value\.batchName/)
assert.match(adoptionDialog, /function invalidatePreview\(\)[\s\S]*?previewToken\.value\s*=\s*['"][\s\S]*?batchName\.value\s*=\s*['"]/)
const managementPath = 'src/views/system/testBatch/index.vue'
const management = await readFile(path.join(frontendRoot, managementPath), 'utf8')
assert.match(management, /const actionsFor\s*=\s*row\s*=>\s*\(\{[\s\S]*?edit:\s*row\.status\s*!==\s*['"]PURGED['"][\s\S]*?assign:\s*row\.status\s*===\s*['"]ACTIVE['"][\s\S]*?close:\s*row\.status\s*===\s*['"]ACTIVE['"][\s\S]*?reopen:\s*row\.status\s*===\s*['"]CLOSED['"][\s\S]*?purgePreview:\s*row\.status\s*===\s*['"]CLOSED['"][\s\S]*?audit:\s*row\.status\s*===\s*['"]PURGED['"][\s\S]*?}\)/)
assert.match(management, /onlinePurgeAllowed\.value\s*===\s*false/)
assert.match(management, /blockingReason/)
assert.match(management, /testBatchStore\.leaveTestMode\(\)/)
const reportPaths = Object.values(reportPageContracts).map((contract) => contract.relativePath)
for (const reportPath of reportPaths) {
const source = await readFile(path.join(frontendRoot, reportPath), 'utf8')
assert.doesNotMatch(source, /TestBatchAdoptionDialog/, `${reportPath} 不得提供纳入入口`)
}
})
@@ -7,3 +7,87 @@ export function listAvailableTestBatches() {
method: 'get'
})
}
export function listTestBatches(params) {
return request({
url: '/system/test-batch/list',
method: 'get',
params: params
})
}
export function getTestBatch(batchId) {
return request({
url: '/system/test-batch/' + batchId,
method: 'get'
})
}
export function addTestBatch(data) {
return request({
url: '/system/test-batch',
method: 'post',
data: data
})
}
export function updateTestBatch(batchId, data) {
return request({
url: '/system/test-batch/' + batchId,
method: 'put',
data: data
})
}
export function assignTestBatchUsers(batchId, data) {
return request({
url: '/system/test-batch/' + batchId + '/users',
method: 'put',
data: data
})
}
export function closeTestBatch(batchId) {
return request({
url: '/system/test-batch/' + batchId + '/close',
method: 'put'
})
}
export function reopenTestBatch(batchId) {
return request({
url: '/system/test-batch/' + batchId + '/reopen',
method: 'put'
})
}
export function previewTestBatchAdoption(batchId, data) {
return request({
url: '/system/test-batch/' + batchId + '/adoption/preview',
method: 'post',
data: data
})
}
export function executeTestBatchAdoption(batchId, data) {
return request({
url: '/system/test-batch/' + batchId + '/adoption/execute',
method: 'post',
data: data
})
}
export function previewTestBatchPurge(batchId) {
return request({
url: '/system/test-batch/' + batchId + '/purge/preview',
method: 'post'
})
}
export function purgeTestBatch(batchId, data) {
return request({
url: '/system/test-batch/' + batchId + '/purge',
method: 'post',
data: data
})
}
@@ -0,0 +1,220 @@
<template>
<el-dialog
v-model="visible"
title="纳入测试批次"
width="min(820px, 94vw)"
top="5vh"
append-to-body
destroy-on-close
:close-on-click-modal="!submitting"
:close-on-press-escape="!submitting"
:show-close="!submitting"
:before-close="beforeClose"
@closed="resetState"
>
<el-alert
title="将所选正式数据及其关联闭包纳入测试批次。请先核对完整预览,再输入批次全名确认。"
type="warning"
:closable="false"
show-icon
class="mb8"
/>
<el-alert v-if="errorMessage" :title="errorMessage" type="error" :closable="false" show-icon class="mb8" />
<el-form label-width="100px" @submit.prevent>
<el-form-item label="目标批次">
<el-select v-model="batchId" :loading="batchLoading" :disabled="batchLoading || previewLoading || submitting" placeholder="暂无可用测试批次" @change="handleBatchChange">
<el-option v-for="item in activeBatches" :key="item.batchId" :label="`${item.batchName} (${item.batchCode})`" :value="item.batchId" />
</el-select>
</el-form-item>
<el-form-item label="选择数量">
<span>{{ roots[0]?.resourceIds.length || 0 }} 条 {{ rootType }}</span>
</el-form-item>
</el-form>
<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-item label="目标批次">{{ preview.batchName }}</el-descriptions-item>
<el-descriptions-item label="影响总数">{{ preview.totalCount }}</el-descriptions-item>
</el-descriptions>
<el-table :data="preview.impacts" size="small" max-height="280" empty-text="无受影响数据">
<el-table-column label="分类" prop="label" min-width="150" :show-overflow-tooltip="true" />
<el-table-column label="数量" prop="count" width="88" align="right" />
<el-table-column label="全部编号" min-width="300">
<template #default="scope">
<span class="impact-keys">{{ (scope.row.keys || []).join(', ') || '-' }}</span>
</template>
</el-table-column>
</el-table>
<el-form label-width="100px" class="confirm-form" @submit.prevent>
<el-form-item label="批次全名">
<el-input v-model="batchName" :disabled="submitting" :placeholder="`请输入 ${preview.batchName}`" maxlength="100" />
</el-form-item>
</el-form>
</template>
<template #footer>
<div class="dialog-footer">
<el-button :disabled="submitting" @click="close">取消</el-button>
<el-button type="primary" :loading="previewLoading" :disabled="batchLoading || previewLoading || submitting || !batchId" @click="requestPreview">重新预览</el-button>
<el-button type="danger" :loading="submitting" :disabled="!canExecute" @click="execute">确认纳入</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup>
import { listAvailableTestBatches, previewTestBatchAdoption, executeTestBatchAdoption } from '@/api/system/testBatch'
const props = defineProps({
rootType: { type: String, required: true },
resourceIds: { type: Array, required: true }
})
const emit = defineEmits(['success'])
const { proxy } = getCurrentInstance()
const visible = ref(false)
const batchLoading = ref(false)
const previewLoading = ref(false)
const submitting = ref(false)
const errorMessage = ref('')
const activeBatches = ref([])
const batchId = ref(undefined)
const preview = ref(null)
const previewToken = ref('')
const batchName = ref('')
let requestVersion = 0
const roots = computed(() => [{
resourceType: props.rootType,
resourceIds: [...new Set(props.resourceIds.map(Number).filter((id) => Number.isInteger(id) && id > 0))]
}])
const canExecute = computed(() => Boolean(
preview.value && previewToken.value && batchName.value === preview.value.batchName && !previewLoading.value && !submitting.value
))
function invalidatePreview() {
requestVersion += 1
previewLoading.value = false
preview.value = null
previewToken.value = ''
batchName.value = ''
}
function resetState() {
invalidatePreview()
batchLoading.value = false
submitting.value = false
errorMessage.value = ''
activeBatches.value = []
batchId.value = undefined
}
function close() {
if (!submitting.value) visible.value = false
}
function beforeClose(done) {
if (submitting.value) return
done()
}
function normalizePreview(data) {
return {
batchName: data.batchName,
totalCount: data.totalCount,
impacts: Array.isArray(data.impacts) ? data.impacts : []
}
}
function requestPreview() {
if (!batchId.value || !roots.value[0].resourceIds.length || previewLoading.value || submitting.value) {
if (!roots.value[0].resourceIds.length) errorMessage.value = '请至少选择一条有效数据'
return
}
invalidatePreview()
const currentVersion = ++requestVersion
const targetBatchId = batchId.value
previewLoading.value = true
errorMessage.value = ''
previewTestBatchAdoption(targetBatchId, { roots: roots.value }).then((response) => {
if (currentVersion !== requestVersion || targetBatchId !== batchId.value) 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
invalidatePreview()
errorMessage.value = '预览已失效或目标批次不可用,请重新预览'
}).finally(() => {
if (currentVersion === requestVersion && targetBatchId === batchId.value) previewLoading.value = false
})
}
function handleBatchChange() {
invalidatePreview()
requestPreview()
}
function loadBatches() {
batchLoading.value = true
errorMessage.value = ''
return listAvailableTestBatches().then((response) => {
activeBatches.value = (Array.isArray(response.data) ? response.data : []).filter((item) => item.status === 'ACTIVE')
batchId.value = activeBatches.value[0]?.batchId
if (!batchId.value) {
errorMessage.value = '暂无可用测试批次'
return
}
requestPreview()
}).catch(() => {
activeBatches.value = []
batchId.value = undefined
errorMessage.value = '测试批次加载失败,请稍后重试'
}).finally(() => {
batchLoading.value = false
})
}
function open() {
if (!roots.value[0].resourceIds.length) {
proxy.$modal.msgWarning('请至少选择一条有效数据')
return
}
resetState()
visible.value = true
loadBatches()
}
function execute() {
if (!preview.value || batchName.value !== preview.value.batchName || !canExecute.value || submitting.value) return
const targetBatchId = batchId.value
const token = previewToken.value
submitting.value = true
errorMessage.value = ''
executeTestBatchAdoption(targetBatchId, { roots: roots.value, batchName: batchName.value, previewToken: previewToken.value }).then((response) => {
proxy.$modal.msgSuccess(response.msg || '数据已纳入测试批次')
visible.value = false
emit('success')
}).catch(() => {
if (targetBatchId !== batchId.value || token !== previewToken.value) return
invalidatePreview()
errorMessage.value = '预览令牌已失效或目标批次已关闭,请重新预览'
}).finally(() => {
submitting.value = false
})
}
watch(() => props.resourceIds, () => {
if (visible.value) invalidatePreview()
}, { deep: true })
defineExpose({ open })
</script>
<style scoped>
.impact-keys { display: block; overflow-wrap: anywhere; white-space: normal; }
.confirm-form { margin-top: 12px; }
</style>
@@ -32,7 +32,9 @@
</el-row>
<el-alert v-if="loadError" :title="loadError" type="error" :closable="false" show-icon class="mb8" />
<el-table v-loading="loading" empty-text="暂无数据" :data="fillingList" class="data-table">
<div v-if="userStore.admin === true && !testBatchStore.isTestMode" class="mb8"><el-button type="warning" plain icon="FolderAdd" :disabled="!selectedIds.length || operationPending" @click="openAdoptionDialog">纳入测试批次</el-button></div>
<el-table v-loading="loading" empty-text="暂无数据" :data="fillingList" class="data-table" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" />
<el-table-column label="装填单号" prop="fillingNo" min-width="170" :show-overflow-tooltip="true" />
<el-table-column label="托盘" min-width="120">
<template #default="scope">{{ scope.row.palletCode || `#${scope.row.palletId}` }}</template>
@@ -56,6 +58,7 @@
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
<test-batch-adoption-dialog ref="adoptionDialogRef" :root-type="'filling'" :resource-ids="selectedIds" @success="getList" />
<el-dialog
v-model="open"
@@ -161,9 +164,16 @@
<script setup name="BusinessFilling">
import PalletScanInput from '@/components/PalletScanInput'
import { createIdempotencyKey } from '@/utils/palletScan'
import TestBatchAdoptionDialog from '@/components/TestBatchAdoptionDialog'
import useUserStore from '@/store/modules/user'
import useTestBatchStore from '@/store/modules/testBatch'
import { addFilling, getFillingStrainOptions, listFilling, scanFillingPallet } from '@/api/business/filling'
const { proxy } = getCurrentInstance()
const userStore = useUserStore()
const testBatchStore = useTestBatchStore()
const adoptionDialogRef = ref()
const selectedIds = ref([])
const fillingList = ref([])
const strainOptions = ref([])
const scannedPallet = ref(null)
@@ -246,6 +256,9 @@ function getList() {
})
}
function handleSelectionChange(selection) { selectedIds.value = selection.map((item) => item.fillingId) }
function openAdoptionDialog() { adoptionDialogRef.value?.open() }
function invalidateOptionRequest() {
optionGeneration += 1
optionLoading.value = false
@@ -41,7 +41,9 @@
</el-row>
<el-alert v-if="loadError" :title="loadError" type="error" :closable="false" show-icon class="mb8" />
<el-table v-loading="loading" empty-text="暂无数据" :data="inboundList" class="data-table">
<div v-if="userStore.admin === true && !testBatchStore.isTestMode" class="mb8"><el-button type="warning" plain icon="FolderAdd" :disabled="!selectedIds.length || operationPending" @click="openAdoptionDialog">纳入测试批次</el-button></div>
<el-table v-loading="loading" empty-text="暂无数据" :data="inboundList" class="data-table" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" />
<el-table-column label="入库单号" prop="inboundNo" min-width="170" :show-overflow-tooltip="true" />
<el-table-column label="仓库" min-width="120">
<template #default="scope">{{ optionLabel(warehouseOptions, scope.row.warehouseId) }}</template>
@@ -99,6 +101,7 @@
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
<test-batch-adoption-dialog ref="adoptionDialogRef" :root-type="'inbound'" :resource-ids="selectedIds" @success="getList" />
<el-dialog
v-model="open"
@@ -199,6 +202,9 @@
<script setup name="BusinessInbound">
import PalletScanInput from '@/components/PalletScanInput'
import { createIdempotencyKey } from '@/utils/palletScan'
import TestBatchAdoptionDialog from '@/components/TestBatchAdoptionDialog'
import useUserStore from '@/store/modules/user'
import useTestBatchStore from '@/store/modules/testBatch'
import {
addInbound,
confirmInbound,
@@ -211,6 +217,10 @@ import {
} from '@/api/business/inbound'
const { proxy } = getCurrentInstance()
const userStore = useUserStore()
const testBatchStore = useTestBatchStore()
const adoptionDialogRef = ref()
const selectedIds = ref([])
const inboundList = ref([])
const warehouseOptions = ref([])
const scannedPallets = ref([])
@@ -287,6 +297,9 @@ function getList() {
})
}
function handleSelectionChange(selection) { selectedIds.value = selection.map((item) => item.inboundId) }
function openAdoptionDialog() { adoptionDialogRef.value?.open() }
function invalidateOptionRequest() {
optionGeneration += 1
optionLoading.value = false
@@ -173,7 +173,9 @@
</el-form>
<el-alert v-if="loadError" :title="loadError" type="error" :closable="false" show-icon class="mb8" />
<el-table v-loading="loading" empty-text="暂无数据" :data="returnList" class="data-table">
<div v-if="userStore.admin === true && !testBatchStore.isTestMode" class="mb8"><el-button type="warning" plain icon="FolderAdd" :disabled="!selectedIds.length || operationPending" @click="openAdoptionDialog">纳入测试批次</el-button></div>
<el-table v-loading="loading" empty-text="暂无数据" :data="returnList" class="data-table" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" />
<el-table-column label="退回单号" prop="returnNo" min-width="170" :show-overflow-tooltip="true" />
<el-table-column label="来源现场" min-width="120">
<template #default="scope">#{{ scope.row.sourceSiteId }}</template>
@@ -212,12 +214,16 @@
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
<test-batch-adoption-dialog ref="adoptionDialogRef" :root-type="'return'" :resource-ids="selectedIds" @success="getList" />
</div>
</template>
<script setup name="BusinessReturn">
import PalletScanInput from '@/components/PalletScanInput'
import { createIdempotencyKey } from '@/utils/palletScan'
import TestBatchAdoptionDialog from '@/components/TestBatchAdoptionDialog'
import useUserStore from '@/store/modules/user'
import useTestBatchStore from '@/store/modules/testBatch'
import {
confirmReturn,
getReturn,
@@ -228,6 +234,10 @@ import {
} from '@/api/business/return'
const { proxy } = getCurrentInstance()
const userStore = useUserStore()
const testBatchStore = useTestBatchStore()
const adoptionDialogRef = ref()
const selectedIds = ref([])
const returnList = ref([])
const warehouseOptions = ref([])
const requestScannedPallets = ref([])
@@ -307,6 +317,9 @@ function getList() {
})
}
function handleSelectionChange(selection) { selectedIds.value = selection.map((item) => item.returnId) }
function openAdoptionDialog() { adoptionDialogRef.value?.open() }
function invalidateOptionRequest() {
optionGeneration += 1
optionLoading.value = false
@@ -38,7 +38,9 @@
</el-row>
<el-alert v-if="loadError" :title="loadError" type="error" :closable="false" show-icon class="mb8" />
<el-table v-loading="loading" empty-text="暂无数据" :data="shipmentList" class="data-table">
<div v-if="userStore.admin === true && !testBatchStore.isTestMode" class="mb8"><el-button type="warning" plain icon="FolderAdd" :disabled="!selectedIds.length || operationPending" @click="openAdoptionDialog">纳入测试批次</el-button></div>
<el-table v-loading="loading" empty-text="暂无数据" :data="shipmentList" class="data-table" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" />
<el-table-column label="发货单号" prop="shipmentNo" min-width="170" :show-overflow-tooltip="true" />
<el-table-column label="来源仓库" min-width="130">
<template #default="scope">{{ optionLabel(warehouseOptions, scope.row.sourceWarehouseId) }}</template>
@@ -96,6 +98,7 @@
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
<test-batch-adoption-dialog ref="adoptionDialogRef" :root-type="'shipment'" :resource-ids="selectedIds" @success="getList" />
<el-dialog
v-model="open"
@@ -210,6 +213,9 @@
<script setup name="BusinessShipment">
import PalletScanInput from '@/components/PalletScanInput'
import { createIdempotencyKey } from '@/utils/palletScan'
import TestBatchAdoptionDialog from '@/components/TestBatchAdoptionDialog'
import useUserStore from '@/store/modules/user'
import useTestBatchStore from '@/store/modules/testBatch'
import {
addShipment,
confirmShipment,
@@ -223,6 +229,10 @@ import {
} from '@/api/business/shipment'
const { proxy } = getCurrentInstance()
const userStore = useUserStore()
const testBatchStore = useTestBatchStore()
const adoptionDialogRef = ref()
const selectedIds = ref([])
const shipmentList = ref([])
const warehouseOptions = ref([])
const siteOptions = ref([])
@@ -301,6 +311,9 @@ function getList() {
})
}
function handleSelectionChange(selection) { selectedIds.value = selection.map((item) => item.shipmentId) }
function openAdoptionDialog() { adoptionDialogRef.value?.open() }
function invalidateOptionRequest() {
optionGeneration += 1
optionLoading.value = false
@@ -112,7 +112,9 @@
</el-form>
<el-alert v-if="loadError" :title="loadError" type="error" :closable="false" show-icon class="mb8" />
<el-table v-loading="loading" empty-text="暂无数据" :data="usageList" class="data-table">
<div v-if="userStore.admin === true && !testBatchStore.isTestMode" class="mb8"><el-button type="warning" plain icon="FolderAdd" :disabled="!selectedIds.length || operationPending" @click="openAdoptionDialog">纳入测试批次</el-button></div>
<el-table v-loading="loading" empty-text="暂无数据" :data="usageList" class="data-table" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" />
<el-table-column label="使用记录号" prop="usageNo" min-width="170" :show-overflow-tooltip="true" />
<el-table-column label="托盘" min-width="120">
<template #default="scope">{{ scope.row.palletCode || `#${scope.row.palletId}` }}</template>
@@ -133,15 +135,23 @@
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
<test-batch-adoption-dialog ref="adoptionDialogRef" :root-type="'usage'" :resource-ids="selectedIds" @success="getList" />
</div>
</template>
<script setup name="BusinessUsage">
import PalletScanInput from '@/components/PalletScanInput'
import { createIdempotencyKey } from '@/utils/palletScan'
import TestBatchAdoptionDialog from '@/components/TestBatchAdoptionDialog'
import useUserStore from '@/store/modules/user'
import useTestBatchStore from '@/store/modules/testBatch'
import { addUsage, getUsageLineOptions, listUsage, scanUsagePallet } from '@/api/business/usage'
const { proxy } = getCurrentInstance()
const userStore = useUserStore()
const testBatchStore = useTestBatchStore()
const adoptionDialogRef = ref()
const selectedIds = ref([])
const usageList = ref([])
const lineOptions = ref([])
const scannedPallet = ref(null)
@@ -212,6 +222,9 @@ function getList() {
})
}
function handleSelectionChange(selection) { selectedIds.value = selection.map((item) => item.usageId) }
function openAdoptionDialog() { adoptionDialogRef.value?.open() }
function invalidateOptionRequest() {
optionGeneration += 1
optionLoading.value = false
@@ -18,7 +18,7 @@
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8 master-toolbar">
<el-row v-if="isMasterWriteAllowed" :gutter="10" class="mb8 master-toolbar">
<el-col :span="1.5"><el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['master:customer:add']">新增</el-button></el-col>
<el-col :span="1.5">
<el-button
@@ -29,6 +29,9 @@
</el-col>
<el-col :span="1.5"><el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['master:customer:remove']">删除</el-button></el-col>
</el-row>
<el-row v-if="userStore.admin === true && !testBatchStore.isTestMode" :gutter="10" class="mb8 master-toolbar">
<el-col :span="1.5"><el-button type="warning" plain icon="FolderAdd" :disabled="!selectedIds.length" @click="openAdoptionDialog">纳入测试批次</el-button></el-col>
</el-row>
<el-alert v-if="loadError" :title="loadError" type="error" :closable="false" show-icon class="mb8" />
<el-table class="master-table data-table" v-loading="loading" empty-text="暂无数据" :data="customerList" @selection-change="handleSelectionChange">
@@ -42,7 +45,7 @@
<el-table-column label="创建时间" prop="createTime" align="center" width="180">
<template #default="scope"><span>{{ parseTime(scope.row.createTime) }}</span></template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<el-table-column v-if="isMasterWriteAllowed" label="操作" align="center" class-name="small-padding fixed-width">
<template #default="scope">
<el-button
v-if="proxy.$auth.hasPermiAnd(['master:customer:query', 'master:customer:edit'])"
@@ -60,6 +63,7 @@
v-show="total > 0" :total="total"
v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList"
/>
<test-batch-adoption-dialog ref="adoptionDialogRef" :root-type="'customer'" :resource-ids="selectedIds" @success="getList" />
<el-dialog
v-model="open"
@@ -114,8 +118,15 @@
<script setup name="Customer">
import { addCustomer, delCustomer, getCustomer, listCustomer, updateCustomer } from "@/api/master/customer";
import TestBatchAdoptionDialog from '@/components/TestBatchAdoptionDialog'
import useUserStore from '@/store/modules/user'
import useTestBatchStore from '@/store/modules/testBatch'
const { proxy } = getCurrentInstance();
const userStore = useUserStore()
const testBatchStore = useTestBatchStore()
const isMasterWriteAllowed = computed(() => userStore.admin === true || !testBatchStore.isTestMode)
const adoptionDialogRef = ref()
const { sys_normal_disable } = proxy.useDict("sys_normal_disable");
const customerList = ref([]);
const selectedIds = ref([]);
@@ -218,6 +229,8 @@ function handleSelectionChange(selection) {
single.value = selectedIds.value.length !== 1;
multiple.value = removableIds.value.length === 0;
}
function openAdoptionDialog() { adoptionDialogRef.value?.open() }
function handleAdd() {
invalidateDetailRequest();
reset();
@@ -21,7 +21,7 @@
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8 master-toolbar">
<el-row v-if="isMasterWriteAllowed" :gutter="10" class="mb8 master-toolbar">
<el-col :span="1.5"><el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['master:line:add']">新增</el-button></el-col>
<el-col :span="1.5">
<el-button
@@ -32,6 +32,9 @@
</el-col>
<el-col :span="1.5"><el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['master:line:remove']">删除</el-button></el-col>
</el-row>
<el-row v-if="userStore.admin === true && !testBatchStore.isTestMode" :gutter="10" class="mb8 master-toolbar">
<el-col :span="1.5"><el-button type="warning" plain icon="FolderAdd" :disabled="!selectedIds.length" @click="openAdoptionDialog">纳入测试批次</el-button></el-col>
</el-row>
<el-alert v-if="loadError" :title="loadError" type="error" :closable="false" show-icon class="mb8" />
<el-table class="master-table data-table" v-loading="loading" empty-text="暂无数据" :data="lineList" @selection-change="handleSelectionChange">
@@ -46,7 +49,7 @@
<el-table-column label="创建时间" prop="createTime" align="center" width="180">
<template #default="scope"><span>{{ parseTime(scope.row.createTime) }}</span></template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<el-table-column v-if="isMasterWriteAllowed" label="操作" align="center" class-name="small-padding fixed-width">
<template #default="scope">
<el-button
v-if="proxy.$auth.hasPermiAnd(['master:line:query', 'master:line:edit'])"
@@ -64,6 +67,7 @@
v-show="total > 0" :total="total"
v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList"
/>
<test-batch-adoption-dialog ref="adoptionDialogRef" :root-type="'line'" :resource-ids="selectedIds" @success="getList" />
<el-dialog
v-model="open"
@@ -116,8 +120,15 @@
<script setup name="Line">
import { addLine, delLine, getLine, listLine, updateLine } from "@/api/master/line";
import TestBatchAdoptionDialog from '@/components/TestBatchAdoptionDialog'
import useUserStore from '@/store/modules/user'
import useTestBatchStore from '@/store/modules/testBatch'
const { proxy } = getCurrentInstance();
const userStore = useUserStore()
const testBatchStore = useTestBatchStore()
const isMasterWriteAllowed = computed(() => userStore.admin === true || !testBatchStore.isTestMode)
const adoptionDialogRef = ref()
const { sys_normal_disable } = proxy.useDict("sys_normal_disable");
const lineList = ref([]);
const selectedIds = ref([]);
@@ -227,6 +238,8 @@ function handleSelectionChange(selection) {
single.value = selectedIds.value.length !== 1;
multiple.value = removableIds.value.length === 0;
}
function openAdoptionDialog() { adoptionDialogRef.value?.open() }
function handleAdd() {
invalidateDetailRequest();
reset();
@@ -15,7 +15,7 @@
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8 master-toolbar">
<el-row v-if="isMasterWriteAllowed" :gutter="10" class="mb8 master-toolbar">
<el-col :span="1.5"><el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['master:pallet:add']">新增</el-button></el-col>
<el-col :span="1.5">
<el-button
@@ -26,6 +26,9 @@
</el-col>
<el-col :span="1.5"><el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['master:pallet:remove']">删除</el-button></el-col>
</el-row>
<el-row v-if="userStore.admin === true && !testBatchStore.isTestMode" :gutter="10" class="mb8 master-toolbar">
<el-col :span="1.5"><el-button type="warning" plain icon="FolderAdd" :disabled="!selectedIds.length" @click="openAdoptionDialog">纳入测试批次</el-button></el-col>
</el-row>
<el-alert v-if="loadError" :title="loadError" type="error" :closable="false" show-icon class="mb8" />
<el-table class="master-table data-table" v-loading="loading" empty-text="暂无数据" :data="palletList" @selection-change="handleSelectionChange">
@@ -39,7 +42,7 @@
<el-table-column label="创建时间" prop="createTime" align="center" width="180">
<template #default="scope"><span>{{ parseTime(scope.row.createTime) }}</span></template>
</el-table-column>
<el-table-column label="操作" align="center" width="250" class-name="small-padding fixed-width">
<el-table-column v-if="isMasterWriteAllowed" label="操作" align="center" width="250" class-name="small-padding fixed-width">
<template #default="scope">
<el-button
link type="primary" icon="Printer" :loading="isPrinting(scope.row.palletId)"
@@ -61,6 +64,7 @@
v-show="total > 0" :total="total"
v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList"
/>
<test-batch-adoption-dialog ref="adoptionDialogRef" :root-type="'pallet'" :resource-ids="selectedIds" @success="getList" />
<el-dialog
v-model="open"
@@ -116,8 +120,15 @@
<script setup name="Pallet">
import { addPallet, delPallet, getPallet, getPalletQrcode, listPallet, updatePallet } from "@/api/master/pallet";
import TestBatchAdoptionDialog from '@/components/TestBatchAdoptionDialog'
import useUserStore from '@/store/modules/user'
import useTestBatchStore from '@/store/modules/testBatch'
const { proxy } = getCurrentInstance();
const userStore = useUserStore()
const testBatchStore = useTestBatchStore()
const isMasterWriteAllowed = computed(() => userStore.admin === true || !testBatchStore.isTestMode)
const adoptionDialogRef = ref()
const { sys_normal_disable } = proxy.useDict("sys_normal_disable");
const palletList = ref([]);
const selectedIds = ref([]);
@@ -227,6 +238,8 @@ function handleSelectionChange(selection) {
single.value = selectedIds.value.length !== 1;
multiple.value = removableIds.value.length === 0;
}
function openAdoptionDialog() { adoptionDialogRef.value?.open() }
function handleAdd() {
invalidateDetailRequest();
reset();
@@ -21,7 +21,7 @@
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8 master-toolbar">
<el-row v-if="isMasterWriteAllowed" :gutter="10" class="mb8 master-toolbar">
<el-col :span="1.5"><el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['master:site:add']">新增</el-button></el-col>
<el-col :span="1.5">
<el-button
@@ -33,6 +33,9 @@
<el-col :span="1.5"><el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['master:site:remove']">删除</el-button></el-col>
<el-col :span="1.5"><el-button type="warning" plain icon="User" :disabled="single" @click="handleAssign()" v-hasPermi="['master:site:assign']">分配用户</el-button></el-col>
</el-row>
<el-row v-if="userStore.admin === true && !testBatchStore.isTestMode" :gutter="10" class="mb8 master-toolbar">
<el-col :span="1.5"><el-button type="warning" plain icon="FolderAdd" :disabled="!selectedIds.length" @click="openAdoptionDialog">纳入测试批次</el-button></el-col>
</el-row>
<el-alert v-if="loadError" :title="loadError" type="error" :closable="false" show-icon class="mb8" />
<el-table class="master-table data-table" v-loading="loading" empty-text="暂无数据" :data="siteList" @selection-change="handleSelectionChange">
@@ -48,7 +51,7 @@
<el-table-column label="创建时间" prop="createTime" align="center" width="180">
<template #default="scope"><span>{{ parseTime(scope.row.createTime) }}</span></template>
</el-table-column>
<el-table-column label="操作" align="center" width="260" class-name="small-padding fixed-width">
<el-table-column v-if="isMasterWriteAllowed" label="操作" align="center" width="260" class-name="small-padding fixed-width">
<template #default="scope">
<el-button link type="primary" icon="User" @click="handleAssign(scope.row)" v-hasPermi="['master:site:assign']">分配</el-button>
<el-button
@@ -67,6 +70,7 @@
v-show="total > 0" :total="total"
v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList"
/>
<test-batch-adoption-dialog ref="adoptionDialogRef" :root-type="'site'" :resource-ids="selectedIds" @success="getList" />
<el-dialog
v-model="open"
@@ -125,8 +129,15 @@
<script setup name="Site">
import BusinessUserAssignDialog from "@/components/BusinessUserAssignDialog";
import { addSite, delSite, getSite, listSite, updateSite } from "@/api/master/site";
import TestBatchAdoptionDialog from '@/components/TestBatchAdoptionDialog'
import useUserStore from '@/store/modules/user'
import useTestBatchStore from '@/store/modules/testBatch'
const { proxy } = getCurrentInstance();
const userStore = useUserStore()
const testBatchStore = useTestBatchStore()
const isMasterWriteAllowed = computed(() => userStore.admin === true || !testBatchStore.isTestMode)
const adoptionDialogRef = ref()
const { sys_normal_disable } = proxy.useDict("sys_normal_disable");
const siteList = ref([]);
const selectedIds = ref([]);
@@ -239,6 +250,8 @@ function handleSelectionChange(selection) {
single.value = selectedIds.value.length !== 1;
multiple.value = removableIds.value.length === 0;
}
function openAdoptionDialog() { adoptionDialogRef.value?.open() }
function handleAdd() {
invalidateDetailRequest();
reset();
@@ -18,7 +18,7 @@
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8 master-toolbar">
<el-row v-if="isMasterWriteAllowed" :gutter="10" class="mb8 master-toolbar">
<el-col :span="1.5">
<el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['master:strain:add']">新增</el-button>
</el-col>
@@ -38,6 +38,9 @@
<el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['master:strain:remove']">删除</el-button>
</el-col>
</el-row>
<el-row v-if="userStore.admin === true && !testBatchStore.isTestMode" :gutter="10" class="mb8 master-toolbar">
<el-col :span="1.5"><el-button type="warning" plain icon="FolderAdd" :disabled="!selectedIds.length" @click="openAdoptionDialog">纳入测试批次</el-button></el-col>
</el-row>
<el-alert v-if="loadError" :title="loadError" type="error" :closable="false" show-icon class="mb8" />
@@ -54,7 +57,7 @@
<el-table-column label="创建时间" prop="createTime" align="center" width="180">
<template #default="scope"><span>{{ parseTime(scope.row.createTime) }}</span></template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<el-table-column v-if="isMasterWriteAllowed" label="操作" align="center" class-name="small-padding fixed-width">
<template #default="scope">
<el-button
v-if="proxy.$auth.hasPermiAnd(['master:strain:query', 'master:strain:edit'])"
@@ -84,6 +87,7 @@
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
<test-batch-adoption-dialog ref="adoptionDialogRef" :root-type="'strain'" :resource-ids="selectedIds" @success="getList" />
<el-dialog
v-model="open"
@@ -133,8 +137,15 @@
<script setup name="Strain">
import { addStrain, delStrain, getStrain, listStrain, updateStrain } from "@/api/master/strain";
import TestBatchAdoptionDialog from '@/components/TestBatchAdoptionDialog'
import useUserStore from '@/store/modules/user'
import useTestBatchStore from '@/store/modules/testBatch'
const { proxy } = getCurrentInstance();
const userStore = useUserStore()
const testBatchStore = useTestBatchStore()
const isMasterWriteAllowed = computed(() => userStore.admin === true || !testBatchStore.isTestMode)
const adoptionDialogRef = ref()
const { sys_normal_disable } = proxy.useDict("sys_normal_disable");
const strainList = ref([]);
@@ -252,6 +263,8 @@ function handleSelectionChange(selection) {
multiple.value = removableIds.value.length === 0;
}
function openAdoptionDialog() { adoptionDialogRef.value?.open() }
function handleAdd() {
invalidateDetailRequest();
reset();
@@ -18,7 +18,7 @@
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8 master-toolbar">
<el-row v-if="isMasterWriteAllowed" :gutter="10" class="mb8 master-toolbar">
<el-col :span="1.5"><el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['master:warehouse:add']">新增</el-button></el-col>
<el-col :span="1.5">
<el-button
@@ -30,6 +30,9 @@
<el-col :span="1.5"><el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['master:warehouse:remove']">删除</el-button></el-col>
<el-col :span="1.5"><el-button type="warning" plain icon="User" :disabled="single" @click="handleAssign()" v-hasPermi="['master:warehouse:assign']">分配用户</el-button></el-col>
</el-row>
<el-row v-if="userStore.admin === true && !testBatchStore.isTestMode" :gutter="10" class="mb8 master-toolbar">
<el-col :span="1.5"><el-button type="warning" plain icon="FolderAdd" :disabled="!selectedIds.length" @click="openAdoptionDialog">纳入测试批次</el-button></el-col>
</el-row>
<el-alert v-if="loadError" :title="loadError" type="error" :closable="false" show-icon class="mb8" />
<el-table class="master-table data-table" v-loading="loading" empty-text="暂无数据" :data="warehouseList" @selection-change="handleSelectionChange">
@@ -44,7 +47,7 @@
<el-table-column label="创建时间" prop="createTime" align="center" width="180">
<template #default="scope"><span>{{ parseTime(scope.row.createTime) }}</span></template>
</el-table-column>
<el-table-column label="操作" align="center" width="260" class-name="small-padding fixed-width">
<el-table-column v-if="isMasterWriteAllowed" label="操作" align="center" width="260" class-name="small-padding fixed-width">
<template #default="scope">
<el-button link type="primary" icon="User" @click="handleAssign(scope.row)" v-hasPermi="['master:warehouse:assign']">分配</el-button>
<el-button
@@ -63,6 +66,7 @@
v-show="total > 0" :total="total"
v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList"
/>
<test-batch-adoption-dialog ref="adoptionDialogRef" :root-type="'warehouse'" :resource-ids="selectedIds" @success="getList" />
<el-dialog
v-model="open"
@@ -118,8 +122,15 @@
<script setup name="Warehouse">
import BusinessUserAssignDialog from "@/components/BusinessUserAssignDialog";
import { addWarehouse, delWarehouse, getWarehouse, listWarehouse, updateWarehouse } from "@/api/master/warehouse";
import TestBatchAdoptionDialog from '@/components/TestBatchAdoptionDialog'
import useUserStore from '@/store/modules/user'
import useTestBatchStore from '@/store/modules/testBatch'
const { proxy } = getCurrentInstance();
const userStore = useUserStore()
const testBatchStore = useTestBatchStore()
const isMasterWriteAllowed = computed(() => userStore.admin === true || !testBatchStore.isTestMode)
const adoptionDialogRef = ref()
const { sys_normal_disable } = proxy.useDict("sys_normal_disable");
const warehouseList = ref([]);
const selectedIds = ref([]);
@@ -224,6 +235,8 @@ function handleSelectionChange(selection) {
single.value = selectedIds.value.length !== 1;
multiple.value = removableIds.value.length === 0;
}
function openAdoptionDialog() { adoptionDialogRef.value?.open() }
function handleAdd() {
invalidateDetailRequest();
reset();
@@ -0,0 +1,210 @@
<template>
<div v-if="userStore.admin === true" class="app-container test-batch-page">
<div class="page-toolbar mb8">
<div>
<h2>测试数据批次</h2>
<p>管理测试数据的生命周期、参与用户和清理审计。</p>
</div>
<el-button type="primary" icon="Plus" :disabled="operationLoading" @click="openCreate">创建批次</el-button>
</div>
<el-alert v-if="loadError" :title="loadError" type="error" :closable="false" show-icon class="mb8" />
<el-table v-loading="loading" :data="batchList" class="data-table" empty-text="暂无测试批次">
<el-table-column label="批次编码" prop="batchCode" min-width="150" :show-overflow-tooltip="true" />
<el-table-column label="批次名称" prop="batchName" min-width="170" :show-overflow-tooltip="true" />
<el-table-column label="状态" prop="status" width="100" align="center">
<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>
</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>
<el-table-column label="清理时间" prop="purgedAt" width="180"><template #default="scope">{{ parseTime(scope.row.purgedAt) }}</template></el-table-column>
<el-table-column label="操作" width="310" fixed="right">
<template #default="scope">
<el-button v-if="actionsFor(scope.row).edit" link type="primary" :disabled="operationLoading" @click="openEdit(scope.row)">编辑</el-button>
<el-button v-if="actionsFor(scope.row).assign" link type="primary" :disabled="operationLoading" @click="openAssign(scope.row)">用户</el-button>
<el-button v-if="actionsFor(scope.row).close" link type="warning" :loading="operationName === `close:${scope.row.batchId}`" :disabled="operationLoading" @click="closeBatch(scope.row)">关闭</el-button>
<el-button v-if="actionsFor(scope.row).reopen" link type="success" :loading="operationName === `reopen:${scope.row.batchId}`" :disabled="operationLoading" @click="reopenBatch(scope.row)">重新开启</el-button>
<el-button v-if="actionsFor(scope.row).purgePreview" link type="danger" :disabled="operationLoading" @click="openPurge(scope.row)">清理</el-button>
<el-button v-if="actionsFor(scope.row).audit" link type="info" :disabled="operationLoading" @click="openAudit(scope.row)">审计</el-button>
</template>
</el-table-column>
</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-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>
<el-form-item label="备注" prop="remark"><el-input v-model="form.remark" type="textarea" :rows="3" :disabled="formSubmitting" maxlength="500" show-word-limit /></el-form-item>
</el-form>
<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-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>
</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-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-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>
<el-descriptions-item label="在线清理">{{ onlinePurgeAllowed ? '允许' : '不允许' }}</el-descriptions-item>
</el-descriptions>
<el-alert v-if="isOnlinePurgeBlocked" :title="purgePreview.blockingReason || '当前范围不允许在线清理'" type="warning" :closable="false" show-icon class="mb8" />
<el-table :data="purgePreview.impacts" size="small" max-height="280" empty-text="无受影响数据">
<el-table-column label="分类" prop="label" min-width="150" />
<el-table-column label="数量" prop="count" width="90" align="right" />
<el-table-column label="全部编号" min-width="320"><template #default="scope"><span class="impact-keys">{{ (scope.row.keys || []).join(', ') || '-' }}</span></template></el-table-column>
</el-table>
<el-form label-width="100px" class="purge-confirm"><el-form-item label="批次全名"><el-input v-model="purgeBatchName" :disabled="purgeSubmitting" :placeholder="`请输入 ${purgePreview.batchName}`" /></el-form-item></el-form>
</template>
<el-empty v-else-if="!purgePreviewLoading && !purgeError" description="暂无清理预览" :image-size="72" />
<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>
</div>
<el-empty v-else description="无测试批次管理权限" />
</template>
<script setup name="TestBatch">
import useUserStore from '@/store/modules/user'
import useTestBatchStore from '@/store/modules/testBatch'
import { addTestBatch, assignTestBatchUsers, closeTestBatch, getTestBatch, listTestBatches, previewTestBatchPurge, purgeTestBatch, reopenTestBatch, updateTestBatch } from '@/api/system/testBatch'
const { proxy } = getCurrentInstance()
const userStore = useUserStore()
const testBatchStore = useTestBatchStore()
const batchList = ref([])
const total = ref(0)
const loading = ref(false)
const loadError = ref('')
const operationLoading = ref(false)
const operationName = ref('')
const formVisible = ref(false)
const formSubmitting = ref(false)
const formError = ref('')
const formRef = ref()
const form = reactive({ batchId: undefined, batchName: '', remark: '' })
const assignVisible = ref(false)
const assignLoading = ref(false)
const assignSubmitting = ref(false)
const assignError = ref('')
const assignBatch = ref(null)
const assignedUserIds = ref('')
const purgeVisible = ref(false)
const purgePreviewLoading = ref(false)
const purgeSubmitting = ref(false)
const purgeError = ref('')
const purgeBatch = ref(null)
const purgePreview = ref(null)
const purgeToken = ref('')
const purgeBatchName = ref('')
const onlinePurgeAllowed = ref(false)
const auditVisible = ref(false)
const auditBatch = ref(null)
let purgeVersion = 0
const queryParams = ref({ pageNum: 1, pageSize: 10 })
const formRules = { batchName: [{ required: true, message: '请输入批次名称', trigger: 'blur' }] }
const actionsFor = row => ({
edit: row.status !== 'PURGED',
assign: row.status === 'ACTIVE',
close: row.status === 'ACTIVE',
reopen: row.status === 'CLOSED',
purgePreview: row.status === 'CLOSED',
audit: row.status === 'PURGED'
})
const canPurge = computed(() => Boolean(purgePreview.value && purgeToken.value && onlinePurgeAllowed.value && purgeBatchName.value === purgePreview.value.batchName && !purgeSubmitting.value))
const isOnlinePurgeBlocked = computed(() => onlinePurgeAllowed.value === false)
function statusType(status) { return status === 'ACTIVE' ? 'success' : status === 'CLOSED' ? 'warning' : 'info' }
function getList() {
loading.value = true
loadError.value = ''
return listTestBatches(queryParams.value).then((response) => {
batchList.value = Array.isArray(response.rows) ? response.rows : []
total.value = response.total || 0
}).catch(() => {
batchList.value = []
total.value = 0
loadError.value = '测试批次列表加载失败,请稍后重试'
}).finally(() => { 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 }
function openEdit(row) { Object.assign(form, { batchId: row.batchId, batchName: row.batchName, remark: row.remark || '' }); formError.value = ''; formVisible.value = true }
function submitForm() {
if (formSubmitting.value) return
formRef.value?.validate((valid) => {
if (!valid) return
formSubmitting.value = true
formError.value = ''
const request = form.batchId ? updateTestBatch(form.batchId, { remark: form.remark }) : addTestBatch({ batchName: form.batchName, remark: form.remark })
request.then(async (response) => { formVisible.value = false; await refreshAfterSuccess(response.msg || '保存成功') }).catch(() => { formError.value = '保存失败,请根据提示修正后重试' }).finally(() => { formSubmitting.value = false })
})
}
function runOperation(name, callback) {
if (operationLoading.value) return
operationLoading.value = true
operationName.value = name
callback().finally(() => { operationLoading.value = false; operationName.value = '' })
}
function closeBatch(row) {
runOperation(`close:${row.batchId}`, () => proxy.$modal.confirm(`确认关闭测试批次“${row.batchName}”吗?`).then(() => closeTestBatch(row.batchId)).then((response) => refreshAfterSuccess(response.msg || '批次已关闭')).catch((error) => { if (error !== 'cancel' && error !== 'close') proxy.$modal.msgError('关闭批次失败') }))
}
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 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 })
}
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 }
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 })
}
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 loadPurgePreview() {
if (!purgeBatch.value || purgePreviewLoading.value || purgeSubmitting.value) return
invalidatePurgePreview(); const version = ++purgeVersion; purgePreviewLoading.value = true; purgeError.value = ''
previewTestBatchPurge(purgeBatch.value.batchId).then((response) => {
if (version !== purgeVersion) 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 })
}
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 })
}
function openAudit(row) { auditBatch.value = row; auditVisible.value = true }
onMounted(getList)
</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%; } }
</style>