From 8221a5162d01337650e891c01df8fce8f5d9ba03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=84=A6=E9=BE=99=E8=A8=80?= Date: Wed, 23 Sep 2026 16:54:22 +0800 Subject: [PATCH] =?UTF-8?q?=E5=89=8D=E7=AB=AF=EF=BC=9A=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E6=89=B9=E6=AC=A1=E7=AE=A1=E7=90=86=E4=B8=8E=E6=97=A7=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E7=BA=B3=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../scripts/verify-business-contract.mjs | 71 ++++++ .../src/api/system/testBatch.js | 84 +++++++ .../TestBatchAdoptionDialog/index.vue | 220 ++++++++++++++++++ .../src/views/business/filling/index.vue | 15 +- .../src/views/business/inbound/index.vue | 15 +- .../src/views/business/return/index.vue | 15 +- .../src/views/business/shipment/index.vue | 15 +- .../src/views/business/usage/index.vue | 15 +- .../src/views/master/customer/index.vue | 17 +- .../src/views/master/line/index.vue | 17 +- .../src/views/master/pallet/index.vue | 17 +- .../src/views/master/site/index.vue | 17 +- .../src/views/master/strain/index.vue | 17 +- .../src/views/master/warehouse/index.vue | 17 +- .../src/views/system/testBatch/index.vue | 210 +++++++++++++++++ 15 files changed, 745 insertions(+), 17 deletions(-) create mode 100644 shierfeng-fastapi-frontend/src/components/TestBatchAdoptionDialog/index.vue create mode 100644 shierfeng-fastapi-frontend/src/views/system/testBatch/index.vue diff --git a/shierfeng-fastapi-frontend/scripts/verify-business-contract.mjs b/shierfeng-fastapi-frontend/scripts/verify-business-contract.mjs index 1446c8e..caab020 100644 --- a/shierfeng-fastapi-frontend/scripts/verify-business-contract.mjs +++ b/shierfeng-fastapi-frontend/scripts/verify-business-contract.mjs @@ -2414,3 +2414,74 @@ test('业务用户分配组件使用固定映射并允许空数组完整替换', assert.match(source, /]*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(`\\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} 不得提供纳入入口`) + } +}) diff --git a/shierfeng-fastapi-frontend/src/api/system/testBatch.js b/shierfeng-fastapi-frontend/src/api/system/testBatch.js index 5035aab..ba60e45 100644 --- a/shierfeng-fastapi-frontend/src/api/system/testBatch.js +++ b/shierfeng-fastapi-frontend/src/api/system/testBatch.js @@ -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 + }) +} diff --git a/shierfeng-fastapi-frontend/src/components/TestBatchAdoptionDialog/index.vue b/shierfeng-fastapi-frontend/src/components/TestBatchAdoptionDialog/index.vue new file mode 100644 index 0000000..aa18312 --- /dev/null +++ b/shierfeng-fastapi-frontend/src/components/TestBatchAdoptionDialog/index.vue @@ -0,0 +1,220 @@ + + + + + diff --git a/shierfeng-fastapi-frontend/src/views/business/filling/index.vue b/shierfeng-fastapi-frontend/src/views/business/filling/index.vue index 2a11e61..78feff1 100644 --- a/shierfeng-fastapi-frontend/src/views/business/filling/index.vue +++ b/shierfeng-fastapi-frontend/src/views/business/filling/index.vue @@ -32,7 +32,9 @@ - +
纳入测试批次
+ + @@ -56,6 +58,7 @@ v-model:limit="queryParams.pageSize" @pagination="getList" /> + 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 diff --git a/shierfeng-fastapi-frontend/src/views/business/inbound/index.vue b/shierfeng-fastapi-frontend/src/views/business/inbound/index.vue index b869004..61670c8 100644 --- a/shierfeng-fastapi-frontend/src/views/business/inbound/index.vue +++ b/shierfeng-fastapi-frontend/src/views/business/inbound/index.vue @@ -41,7 +41,9 @@ - +
纳入测试批次
+ + @@ -99,6 +101,7 @@ v-model:limit="queryParams.pageSize" @pagination="getList" /> + 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 diff --git a/shierfeng-fastapi-frontend/src/views/business/return/index.vue b/shierfeng-fastapi-frontend/src/views/business/return/index.vue index 1d43397..6a539de 100644 --- a/shierfeng-fastapi-frontend/src/views/business/return/index.vue +++ b/shierfeng-fastapi-frontend/src/views/business/return/index.vue @@ -173,7 +173,9 @@ - +
纳入测试批次
+ + @@ -212,12 +214,16 @@ v-model:limit="queryParams.pageSize" @pagination="getList" /> + + +