From 1f23d92ffb816c6f9f0d51f4e358c3c50101ebc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=84=A6=E9=BE=99=E8=A8=80?= Date: Mon, 27 Jul 2026 03:17:52 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8A=9F=E8=83=BD=EF=BC=9A=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E4=B8=9A=E5=8A=A1=E6=8E=A5=E5=8F=A3=E4=B8=8E=E6=89=AB=E7=A0=81?= =?UTF-8?q?=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../scripts/verify-business-contract.mjs | 250 ++++++++++++++++++ .../src/api/business/filling.js | 43 +++ .../src/api/business/inbound.js | 70 +++++ .../src/api/business/return.js | 53 ++++ .../src/api/business/shipment.js | 78 ++++++ .../src/api/business/usage.js | 44 +++ .../src/api/master/customer.js | 44 +++ .../src/api/master/line.js | 44 +++ .../src/api/master/pallet.js | 53 ++++ .../src/api/master/site.js | 61 +++++ .../src/api/master/strain.js | 44 +++ .../src/api/master/warehouse.js | 61 +++++ .../src/api/report/consumption.js | 10 + .../src/api/report/filling.js | 10 + .../src/api/report/palletInventory.js | 10 + .../src/api/report/palletTrace.js | 10 + .../src/api/report/return.js | 10 + .../src/api/report/shipment.js | 10 + .../src/components/PalletScanInput/index.vue | 173 ++++++++++++ .../src/utils/palletScan.js | 17 ++ 20 files changed, 1095 insertions(+) create mode 100644 shierfeng-fastapi-frontend/scripts/verify-business-contract.mjs create mode 100644 shierfeng-fastapi-frontend/src/api/business/filling.js create mode 100644 shierfeng-fastapi-frontend/src/api/business/inbound.js create mode 100644 shierfeng-fastapi-frontend/src/api/business/return.js create mode 100644 shierfeng-fastapi-frontend/src/api/business/shipment.js create mode 100644 shierfeng-fastapi-frontend/src/api/business/usage.js create mode 100644 shierfeng-fastapi-frontend/src/api/master/customer.js create mode 100644 shierfeng-fastapi-frontend/src/api/master/line.js create mode 100644 shierfeng-fastapi-frontend/src/api/master/pallet.js create mode 100644 shierfeng-fastapi-frontend/src/api/master/site.js create mode 100644 shierfeng-fastapi-frontend/src/api/master/strain.js create mode 100644 shierfeng-fastapi-frontend/src/api/master/warehouse.js create mode 100644 shierfeng-fastapi-frontend/src/api/report/consumption.js create mode 100644 shierfeng-fastapi-frontend/src/api/report/filling.js create mode 100644 shierfeng-fastapi-frontend/src/api/report/palletInventory.js create mode 100644 shierfeng-fastapi-frontend/src/api/report/palletTrace.js create mode 100644 shierfeng-fastapi-frontend/src/api/report/return.js create mode 100644 shierfeng-fastapi-frontend/src/api/report/shipment.js create mode 100644 shierfeng-fastapi-frontend/src/components/PalletScanInput/index.vue create mode 100644 shierfeng-fastapi-frontend/src/utils/palletScan.js diff --git a/shierfeng-fastapi-frontend/scripts/verify-business-contract.mjs b/shierfeng-fastapi-frontend/scripts/verify-business-contract.mjs new file mode 100644 index 0000000..31e8513 --- /dev/null +++ b/shierfeng-fastapi-frontend/scripts/verify-business-contract.mjs @@ -0,0 +1,250 @@ +import assert from 'node:assert/strict' +import { readFile } from 'node:fs/promises' +import { test } from 'node:test' +import { fileURLToPath } from 'node:url' +import path from 'node:path' + +const frontendRoot = fileURLToPath(new URL('..', import.meta.url)) + +const expectedRequests = { + 'src/api/master/strain.js': [ + { url: "'/master/strain/list'", method: 'get' }, + { url: "'/master/strain/' + strainId", method: 'get' }, + { url: "'/master/strain'", method: 'post' }, + { url: "'/master/strain'", method: 'put' }, + { url: "'/master/strain/' + strainIds", method: 'delete' } + ], + 'src/api/master/customer.js': [ + { url: "'/master/customer/list'", method: 'get' }, + { url: "'/master/customer/' + customerId", method: 'get' }, + { url: "'/master/customer'", method: 'post' }, + { url: "'/master/customer'", method: 'put' }, + { url: "'/master/customer/' + customerIds", method: 'delete' } + ], + 'src/api/master/site.js': [ + { url: "'/master/site/list'", method: 'get' }, + { url: "'/master/site/' + siteId", method: 'get' }, + { url: "'/master/site'", method: 'post' }, + { url: "'/master/site'", method: 'put' }, + { url: "'/master/site/' + siteIds", method: 'delete' }, + { url: "'/master/site/' + siteId + '/users'", method: 'get' }, + { url: "'/master/site/' + siteId + '/users'", method: 'put' } + ], + 'src/api/master/line.js': [ + { url: "'/master/line/list'", method: 'get' }, + { url: "'/master/line/' + lineId", method: 'get' }, + { url: "'/master/line'", method: 'post' }, + { url: "'/master/line'", method: 'put' }, + { url: "'/master/line/' + lineIds", method: 'delete' } + ], + 'src/api/master/warehouse.js': [ + { url: "'/master/warehouse/list'", method: 'get' }, + { url: "'/master/warehouse/' + warehouseId", method: 'get' }, + { url: "'/master/warehouse'", method: 'post' }, + { url: "'/master/warehouse'", method: 'put' }, + { url: "'/master/warehouse/' + warehouseIds", method: 'delete' }, + { url: "'/master/warehouse/' + warehouseId + '/users'", method: 'get' }, + { url: "'/master/warehouse/' + warehouseId + '/users'", method: 'put' } + ], + 'src/api/master/pallet.js': [ + { url: "'/master/pallet/list'", method: 'get' }, + { url: "'/master/pallet/' + palletId", method: 'get' }, + { url: "'/master/pallet/' + palletId + '/qrcode'", method: 'get' }, + { url: "'/master/pallet'", method: 'post' }, + { url: "'/master/pallet'", method: 'put' }, + { url: "'/master/pallet/' + palletIds", method: 'delete' } + ], + 'src/api/business/inbound.js': [ + { url: "'/business/inbound/list'", method: 'get' }, + { url: "'/business/inbound/warehouse-options'", method: 'get' }, + { url: "'/business/inbound/scan/' + qrToken", method: 'get' }, + { url: "'/business/inbound/' + inboundId", method: 'get' }, + { url: "'/business/inbound'", method: 'post' }, + { url: "'/business/inbound'", method: 'put' }, + { url: "'/business/inbound/' + inboundIds", method: 'delete' }, + { url: "'/business/inbound/' + inboundId + '/confirm'", method: 'put' } + ], + 'src/api/business/filling.js': [ + { url: "'/business/filling/list'", method: 'get' }, + { url: "'/business/filling/strain-options'", method: 'get' }, + { url: "'/business/filling/scan/' + qrToken", method: 'get' }, + { url: "'/business/filling/' + fillingId", method: 'get' }, + { url: "'/business/filling'", method: 'post' } + ], + 'src/api/business/shipment.js': [ + { url: "'/business/shipment/list'", method: 'get' }, + { url: "'/business/shipment/warehouse-options'", method: 'get' }, + { url: "'/business/shipment/site-options'", method: 'get' }, + { url: "'/business/shipment/scan/' + qrToken", method: 'get' }, + { url: "'/business/shipment/' + shipmentId", method: 'get' }, + { url: "'/business/shipment'", method: 'post' }, + { url: "'/business/shipment'", method: 'put' }, + { url: "'/business/shipment/' + shipmentIds", method: 'delete' }, + { url: "'/business/shipment/' + shipmentId + '/confirm'", method: 'put' } + ], + 'src/api/business/usage.js': [ + { url: "'/business/usage/list'", method: 'get' }, + { url: "'/business/usage/line-options'", method: 'get' }, + { url: "'/business/usage/scan/' + qrToken", method: 'get' }, + { url: "'/business/usage/' + usageId", method: 'get' }, + { url: "'/business/usage'", method: 'post' } + ], + 'src/api/business/return.js': [ + { url: "'/business/return/list'", method: 'get' }, + { url: "'/business/return/warehouse-options'", method: 'get' }, + { url: "'/business/return/scan/' + qrToken", method: 'get' }, + { url: "'/business/return/request'", method: 'post' }, + { url: "'/business/return/' + returnId", method: 'get' }, + { url: "'/business/return/' + returnId + '/confirm'", method: 'put' } + ], + 'src/api/report/palletInventory.js': [ + { url: "'/report/pallet-inventory/list'", method: 'get' } + ], + 'src/api/report/filling.js': [ + { url: "'/report/filling/list'", method: 'get' } + ], + 'src/api/report/shipment.js': [ + { url: "'/report/shipment/list'", method: 'get' } + ], + 'src/api/report/consumption.js': [ + { url: "'/report/consumption/list'", method: 'get' } + ], + 'src/api/report/return.js': [ + { url: "'/report/return/list'", method: 'get' } + ], + 'src/api/report/palletTrace.js': [ + { url: "'/report/pallet-trace/list'", method: 'get' } + ] +} + +function extractRequests(source) { + const requests = [] + const requestPattern = /return\s+request\(\{\s*url:\s*([^,\r\n]+),\s*method:\s*['"]([a-z]+)['"]/g + let match + + while ((match = requestPattern.exec(source)) !== null) { + requests.push({ + url: match[1].replace(/\s+/g, ' ').trim(), + method: match[2] + }) + } + + return requests +} + +function hasExpectedRequest(expectedRequest) { + return Object.values(expectedRequests).some((requests) => + requests.some((request) => + request.url === expectedRequest.url && request.method === expectedRequest.method + ) + ) +} + +test('扫码值会去除首尾空白,空白二维码会被拒绝', async () => { + const { normalizeScanValue } = await import('../src/utils/palletScan.js') + + assert.equal(normalizeScanValue(' qr-12345678\n'), 'qr-12345678') + assert.throws(() => normalizeScanValue(' \n\t '), /二维码不能为空/) +}) + +test('重复扫码仅在相同值和时间窗口内成立', async () => { + const { isDuplicateScan } = await import('../src/utils/palletScan.js') + + assert.equal(isDuplicateScan('qr-a', 'qr-a', 1200, 1000, 500), true) + assert.equal(isDuplicateScan('qr-a', 'qr-b', 1200, 1000, 500), false) +}) + +test('幂等键由 Web Crypto randomUUID 生成且不降级为伪随机值', async () => { + const palletScanPath = path.join(frontendRoot, 'src/utils/palletScan.js') + const source = await readFile(palletScanPath, 'utf8') + const { createIdempotencyKey } = await import('../src/utils/palletScan.js') + + assert.match(source, /globalThis\.crypto\.randomUUID\(\)/) + assert.doesNotMatch(source, /Date\.now|Math\.random|new Date/) + assert.match(createIdempotencyKey(), /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i) +}) + +test('17 个 API 文件逐一匹配硬编码请求契约', async (t) => { + assert.equal(Object.keys(expectedRequests).length, 17) + assert.equal(Object.values(expectedRequests).flat().length, 74) + + for (const [relativePath, expected] of Object.entries(expectedRequests)) { + await t.test(relativePath, async () => { + const source = await readFile(path.join(frontendRoot, relativePath), 'utf8') + const actual = extractRequests(source) + const requestCallCount = source.match(/\brequest\s*\(\{/g)?.length ?? 0 + + assert.equal(actual.length, requestCallCount, '存在未被契约解析器覆盖的 request 调用') + assert.deepEqual(actual, expected) + }) + } +}) + +test('关键扫码、选项、确认与二维码契约被显式覆盖', () => { + const requiredRequests = [ + { url: "'/business/inbound/scan/' + qrToken", method: 'get' }, + { url: "'/business/filling/scan/' + qrToken", method: 'get' }, + { url: "'/business/shipment/scan/' + qrToken", method: 'get' }, + { url: "'/business/usage/scan/' + qrToken", method: 'get' }, + { url: "'/business/return/scan/' + qrToken", method: 'get' }, + { url: "'/business/inbound/warehouse-options'", method: 'get' }, + { url: "'/business/filling/strain-options'", method: 'get' }, + { url: "'/business/shipment/warehouse-options'", method: 'get' }, + { url: "'/business/shipment/site-options'", method: 'get' }, + { url: "'/business/usage/line-options'", method: 'get' }, + { url: "'/business/return/warehouse-options'", method: 'get' }, + { url: "'/business/inbound/' + inboundId + '/confirm'", method: 'put' }, + { url: "'/business/shipment/' + shipmentId + '/confirm'", method: 'put' }, + { url: "'/business/return/' + returnId + '/confirm'", method: 'put' }, + { url: "'/business/return/request'", method: 'post' }, + { url: "'/master/pallet/' + palletId + '/qrcode'", method: 'get' } + ] + + for (const requiredRequest of requiredRequests) { + assert.equal(hasExpectedRequest(requiredRequest), true, JSON.stringify(requiredRequest)) + } +}) + +test('六个报表 API 只提供 GET /list,不提供导出接口', async () => { + const reportEntries = Object.entries(expectedRequests).filter(([relativePath]) => + relativePath.startsWith('src/api/report/') + ) + + assert.equal(reportEntries.length, 6) + for (const [relativePath, expected] of reportEntries) { + assert.equal(expected.length, 1) + assert.equal(expected[0].method, 'get') + assert.match(expected[0].url, /^'\/report\/.+\/list'$/) + + const source = await readFile(path.join(frontendRoot, relativePath), 'utf8') + const exportedFunctionNames = [...source.matchAll(/\bexport\s+function\s+(\w+)/g)].map((match) => match[1]) + assert.doesNotMatch(expected[0].url, /export/i) + assert.equal(exportedFunctionNames.some((name) => /export/i.test(name)), false) + } +}) + +test('扫码组件声明交互契约并保持触控尺寸', async () => { + const source = await readFile(path.join(frontendRoot, 'src/components/PalletScanInput/index.vue'), 'utf8') + + for (const propName of ['disabled', 'loading', 'minLength', 'duplicateWindowMs']) { + assert.match(source, new RegExp(`\\b${propName}\\b`)) + } + for (const eventName of ['scan', 'invalid', 'duplicate']) { + assert.match(source, new RegExp(`['"]${eventName}['"]`)) + } + assert.match(source, /@keyup\.enter="handleSubmit"/) + assert.match(source, /normalizeScanValue\(scanValue\.value\)/) + assert.match(source, /isDuplicateScan\(/) + assert.match(source, /emit\(['"]invalid['"]/) + assert.match(source, /emit\(['"]duplicate['"]/) + assert.match(source, /emit\(['"]scan['"]/) + assert.match(source, /nextTick\(/) + assert.match(source, /inputRef\.value\?\.focus\(\)/) + assert.match(source, /min-height:\s*48px/) + assert.match(source, /width:\s*100%/) + assert.match(source, /is-loading/) + assert.match(source, /is-disabled/) + assert.match(source, /is-error/) + assert.match(source, /focus-within/) + assert.doesNotMatch(source, /BarcodeDetector|camera|getUserMedia/i) +}) diff --git a/shierfeng-fastapi-frontend/src/api/business/filling.js b/shierfeng-fastapi-frontend/src/api/business/filling.js new file mode 100644 index 0000000..dedd75c --- /dev/null +++ b/shierfeng-fastapi-frontend/src/api/business/filling.js @@ -0,0 +1,43 @@ +import request from '@/utils/request' + +// 查询托盘装填记录列表 +export function listFilling(query) { + return request({ + url: '/business/filling/list', + method: 'get', + params: query + }) +} + +// 查询装填菌种选项 +export function getFillingStrainOptions() { + return request({ + url: '/business/filling/strain-options', + method: 'get' + }) +} + +// 扫描待装填托盘 +export function scanFillingPallet(qrToken) { + return request({ + url: '/business/filling/scan/' + qrToken, + method: 'get' + }) +} + +// 查询托盘装填记录详情 +export function getFilling(fillingId) { + return request({ + url: '/business/filling/' + fillingId, + method: 'get' + }) +} + +// 记录托盘装填 +export function addFilling(data) { + return request({ + url: '/business/filling', + method: 'post', + data: data + }) +} diff --git a/shierfeng-fastapi-frontend/src/api/business/inbound.js b/shierfeng-fastapi-frontend/src/api/business/inbound.js new file mode 100644 index 0000000..4fb949f --- /dev/null +++ b/shierfeng-fastapi-frontend/src/api/business/inbound.js @@ -0,0 +1,70 @@ +import request from '@/utils/request' + +// 查询首次入库单列表 +export function listInbound(query) { + return request({ + url: '/business/inbound/list', + method: 'get', + params: query + }) +} + +// 查询首次入库仓库选项 +export function getInboundWarehouseOptions() { + return request({ + url: '/business/inbound/warehouse-options', + method: 'get' + }) +} + +// 扫描首次入库托盘 +export function scanInboundPallet(qrToken, query) { + return request({ + url: '/business/inbound/scan/' + qrToken, + method: 'get', + params: query + }) +} + +// 查询首次入库单详情 +export function getInbound(inboundId) { + return request({ + url: '/business/inbound/' + inboundId, + method: 'get' + }) +} + +// 新增首次入库草稿 +export function addInbound(data) { + return request({ + url: '/business/inbound', + method: 'post', + data: data + }) +} + +// 修改首次入库草稿 +export function updateInbound(data) { + return request({ + url: '/business/inbound', + method: 'put', + data: data + }) +} + +// 删除首次入库草稿 +export function delInbound(inboundIds) { + return request({ + url: '/business/inbound/' + inboundIds, + method: 'delete' + }) +} + +// 确认首次入库单 +export function confirmInbound(inboundId, data) { + return request({ + url: '/business/inbound/' + inboundId + '/confirm', + method: 'put', + data: data + }) +} diff --git a/shierfeng-fastapi-frontend/src/api/business/return.js b/shierfeng-fastapi-frontend/src/api/business/return.js new file mode 100644 index 0000000..2fbdc96 --- /dev/null +++ b/shierfeng-fastapi-frontend/src/api/business/return.js @@ -0,0 +1,53 @@ +import request from '@/utils/request' + +// 查询托盘退回单列表 +export function listReturn(query) { + return request({ + url: '/business/return/list', + method: 'get', + params: query + }) +} + +// 查询退回目标仓库选项 +export function getReturnWarehouseOptions() { + return request({ + url: '/business/return/warehouse-options', + method: 'get' + }) +} + +// 扫描退回托盘 +export function scanReturnPallet(qrToken, query) { + return request({ + url: '/business/return/scan/' + qrToken, + method: 'get', + params: query + }) +} + +// 发起托盘退回申请 +export function requestReturn(data) { + return request({ + url: '/business/return/request', + method: 'post', + data: data + }) +} + +// 查询托盘退回单详情 +export function getReturn(returnId) { + return request({ + url: '/business/return/' + returnId, + method: 'get' + }) +} + +// 确认托盘退回 +export function confirmReturn(returnId, data) { + return request({ + url: '/business/return/' + returnId + '/confirm', + method: 'put', + data: data + }) +} diff --git a/shierfeng-fastapi-frontend/src/api/business/shipment.js b/shierfeng-fastapi-frontend/src/api/business/shipment.js new file mode 100644 index 0000000..c7e0505 --- /dev/null +++ b/shierfeng-fastapi-frontend/src/api/business/shipment.js @@ -0,0 +1,78 @@ +import request from '@/utils/request' + +// 查询整托发货单列表 +export function listShipment(query) { + return request({ + url: '/business/shipment/list', + method: 'get', + params: query + }) +} + +// 查询发货来源仓库选项 +export function getShipmentWarehouseOptions() { + return request({ + url: '/business/shipment/warehouse-options', + method: 'get' + }) +} + +// 查询发货目标现场选项 +export function getShipmentSiteOptions() { + return request({ + url: '/business/shipment/site-options', + method: 'get' + }) +} + +// 扫描整托发货托盘 +export function scanShipmentPallet(qrToken, query) { + return request({ + url: '/business/shipment/scan/' + qrToken, + method: 'get', + params: query + }) +} + +// 查询整托发货单详情 +export function getShipment(shipmentId) { + return request({ + url: '/business/shipment/' + shipmentId, + method: 'get' + }) +} + +// 新增整托发货草稿 +export function addShipment(data) { + return request({ + url: '/business/shipment', + method: 'post', + data: data + }) +} + +// 修改整托发货草稿 +export function updateShipment(data) { + return request({ + url: '/business/shipment', + method: 'put', + data: data + }) +} + +// 删除整托发货草稿 +export function delShipment(shipmentIds) { + return request({ + url: '/business/shipment/' + shipmentIds, + method: 'delete' + }) +} + +// 确认整托发货单 +export function confirmShipment(shipmentId, data) { + return request({ + url: '/business/shipment/' + shipmentId + '/confirm', + method: 'put', + data: data + }) +} diff --git a/shierfeng-fastapi-frontend/src/api/business/usage.js b/shierfeng-fastapi-frontend/src/api/business/usage.js new file mode 100644 index 0000000..514a7c2 --- /dev/null +++ b/shierfeng-fastapi-frontend/src/api/business/usage.js @@ -0,0 +1,44 @@ +import request from '@/utils/request' + +// 查询客户现场使用记录列表 +export function listUsage(query) { + return request({ + url: '/business/usage/list', + method: 'get', + params: query + }) +} + +// 查询客户现场启用产线选项 +export function getUsageLineOptions(siteId) { + return request({ + url: '/business/usage/line-options', + method: 'get', + params: { siteId } + }) +} + +// 扫描客户现场待使用托盘 +export function scanUsagePallet(qrToken) { + return request({ + url: '/business/usage/scan/' + qrToken, + method: 'get' + }) +} + +// 查询客户现场使用记录详情 +export function getUsage(usageId) { + return request({ + url: '/business/usage/' + usageId, + method: 'get' + }) +} + +// 记录客户现场使用 +export function addUsage(data) { + return request({ + url: '/business/usage', + method: 'post', + data: data + }) +} diff --git a/shierfeng-fastapi-frontend/src/api/master/customer.js b/shierfeng-fastapi-frontend/src/api/master/customer.js new file mode 100644 index 0000000..5e46fbb --- /dev/null +++ b/shierfeng-fastapi-frontend/src/api/master/customer.js @@ -0,0 +1,44 @@ +import request from '@/utils/request' + +// 查询客户列表 +export function listCustomer(query) { + return request({ + url: '/master/customer/list', + method: 'get', + params: query + }) +} + +// 查询客户详情 +export function getCustomer(customerId) { + return request({ + url: '/master/customer/' + customerId, + method: 'get' + }) +} + +// 新增客户 +export function addCustomer(data) { + return request({ + url: '/master/customer', + method: 'post', + data: data + }) +} + +// 修改客户 +export function updateCustomer(data) { + return request({ + url: '/master/customer', + method: 'put', + data: data + }) +} + +// 删除客户 +export function delCustomer(customerIds) { + return request({ + url: '/master/customer/' + customerIds, + method: 'delete' + }) +} diff --git a/shierfeng-fastapi-frontend/src/api/master/line.js b/shierfeng-fastapi-frontend/src/api/master/line.js new file mode 100644 index 0000000..5d98b33 --- /dev/null +++ b/shierfeng-fastapi-frontend/src/api/master/line.js @@ -0,0 +1,44 @@ +import request from '@/utils/request' + +// 查询生产线列表 +export function listLine(query) { + return request({ + url: '/master/line/list', + method: 'get', + params: query + }) +} + +// 查询生产线详情 +export function getLine(lineId) { + return request({ + url: '/master/line/' + lineId, + method: 'get' + }) +} + +// 新增生产线 +export function addLine(data) { + return request({ + url: '/master/line', + method: 'post', + data: data + }) +} + +// 修改生产线 +export function updateLine(data) { + return request({ + url: '/master/line', + method: 'put', + data: data + }) +} + +// 删除生产线 +export function delLine(lineIds) { + return request({ + url: '/master/line/' + lineIds, + method: 'delete' + }) +} diff --git a/shierfeng-fastapi-frontend/src/api/master/pallet.js b/shierfeng-fastapi-frontend/src/api/master/pallet.js new file mode 100644 index 0000000..4a398b3 --- /dev/null +++ b/shierfeng-fastapi-frontend/src/api/master/pallet.js @@ -0,0 +1,53 @@ +import request from '@/utils/request' + +// 查询托盘列表 +export function listPallet(query) { + return request({ + url: '/master/pallet/list', + method: 'get', + params: query + }) +} + +// 查询托盘详情 +export function getPallet(palletId) { + return request({ + url: '/master/pallet/' + palletId, + method: 'get' + }) +} + +// 获取托盘二维码图片 +export function getPalletQrcode(palletId) { + return request({ + url: '/master/pallet/' + palletId + '/qrcode', + method: 'get', + responseType: 'blob' + }) +} + +// 新增托盘 +export function addPallet(data) { + return request({ + url: '/master/pallet', + method: 'post', + data: data + }) +} + +// 修改托盘 +export function updatePallet(data) { + return request({ + url: '/master/pallet', + method: 'put', + data: data + }) +} + +// 删除托盘 +export function delPallet(palletIds) { + return request({ + url: '/master/pallet/' + palletIds, + method: 'delete' + }) +} diff --git a/shierfeng-fastapi-frontend/src/api/master/site.js b/shierfeng-fastapi-frontend/src/api/master/site.js new file mode 100644 index 0000000..82fbe8b --- /dev/null +++ b/shierfeng-fastapi-frontend/src/api/master/site.js @@ -0,0 +1,61 @@ +import request from '@/utils/request' + +// 查询客户现场列表 +export function listSite(query) { + return request({ + url: '/master/site/list', + method: 'get', + params: query + }) +} + +// 查询客户现场详情 +export function getSite(siteId) { + return request({ + url: '/master/site/' + siteId, + method: 'get' + }) +} + +// 新增客户现场 +export function addSite(data) { + return request({ + url: '/master/site', + method: 'post', + data: data + }) +} + +// 修改客户现场 +export function updateSite(data) { + return request({ + url: '/master/site', + method: 'put', + data: data + }) +} + +// 删除客户现场 +export function delSite(siteIds) { + return request({ + url: '/master/site/' + siteIds, + method: 'delete' + }) +} + +// 查询客户现场可分配用户 +export function getSiteUsers(siteId) { + return request({ + url: '/master/site/' + siteId + '/users', + method: 'get' + }) +} + +// 分配客户现场用户 +export function assignSiteUsers(siteId, data) { + return request({ + url: '/master/site/' + siteId + '/users', + method: 'put', + data: data + }) +} diff --git a/shierfeng-fastapi-frontend/src/api/master/strain.js b/shierfeng-fastapi-frontend/src/api/master/strain.js new file mode 100644 index 0000000..b5a3c61 --- /dev/null +++ b/shierfeng-fastapi-frontend/src/api/master/strain.js @@ -0,0 +1,44 @@ +import request from '@/utils/request' + +// 查询菌种列表 +export function listStrain(query) { + return request({ + url: '/master/strain/list', + method: 'get', + params: query + }) +} + +// 查询菌种详情 +export function getStrain(strainId) { + return request({ + url: '/master/strain/' + strainId, + method: 'get' + }) +} + +// 新增菌种 +export function addStrain(data) { + return request({ + url: '/master/strain', + method: 'post', + data: data + }) +} + +// 修改菌种 +export function updateStrain(data) { + return request({ + url: '/master/strain', + method: 'put', + data: data + }) +} + +// 删除菌种 +export function delStrain(strainIds) { + return request({ + url: '/master/strain/' + strainIds, + method: 'delete' + }) +} diff --git a/shierfeng-fastapi-frontend/src/api/master/warehouse.js b/shierfeng-fastapi-frontend/src/api/master/warehouse.js new file mode 100644 index 0000000..e311e3f --- /dev/null +++ b/shierfeng-fastapi-frontend/src/api/master/warehouse.js @@ -0,0 +1,61 @@ +import request from '@/utils/request' + +// 查询仓库列表 +export function listWarehouse(query) { + return request({ + url: '/master/warehouse/list', + method: 'get', + params: query + }) +} + +// 查询仓库详情 +export function getWarehouse(warehouseId) { + return request({ + url: '/master/warehouse/' + warehouseId, + method: 'get' + }) +} + +// 新增仓库 +export function addWarehouse(data) { + return request({ + url: '/master/warehouse', + method: 'post', + data: data + }) +} + +// 修改仓库 +export function updateWarehouse(data) { + return request({ + url: '/master/warehouse', + method: 'put', + data: data + }) +} + +// 删除仓库 +export function delWarehouse(warehouseIds) { + return request({ + url: '/master/warehouse/' + warehouseIds, + method: 'delete' + }) +} + +// 查询仓库可分配用户 +export function getWarehouseUsers(warehouseId) { + return request({ + url: '/master/warehouse/' + warehouseId + '/users', + method: 'get' + }) +} + +// 分配仓库用户 +export function assignWarehouseUsers(warehouseId, data) { + return request({ + url: '/master/warehouse/' + warehouseId + '/users', + method: 'put', + data: data + }) +} diff --git a/shierfeng-fastapi-frontend/src/api/report/consumption.js b/shierfeng-fastapi-frontend/src/api/report/consumption.js new file mode 100644 index 0000000..ea1e54b --- /dev/null +++ b/shierfeng-fastapi-frontend/src/api/report/consumption.js @@ -0,0 +1,10 @@ +import request from '@/utils/request' + +// 查询产线消耗报表 +export function listConsumptionReport(query) { + return request({ + url: '/report/consumption/list', + method: 'get', + params: query + }) +} diff --git a/shierfeng-fastapi-frontend/src/api/report/filling.js b/shierfeng-fastapi-frontend/src/api/report/filling.js new file mode 100644 index 0000000..efa9220 --- /dev/null +++ b/shierfeng-fastapi-frontend/src/api/report/filling.js @@ -0,0 +1,10 @@ +import request from '@/utils/request' + +// 查询装填记录报表 +export function listFillingReport(query) { + return request({ + url: '/report/filling/list', + method: 'get', + params: query + }) +} diff --git a/shierfeng-fastapi-frontend/src/api/report/palletInventory.js b/shierfeng-fastapi-frontend/src/api/report/palletInventory.js new file mode 100644 index 0000000..4afad39 --- /dev/null +++ b/shierfeng-fastapi-frontend/src/api/report/palletInventory.js @@ -0,0 +1,10 @@ +import request from '@/utils/request' + +// 查询托盘库存报表 +export function listPalletInventory(query) { + return request({ + url: '/report/pallet-inventory/list', + method: 'get', + params: query + }) +} diff --git a/shierfeng-fastapi-frontend/src/api/report/palletTrace.js b/shierfeng-fastapi-frontend/src/api/report/palletTrace.js new file mode 100644 index 0000000..ce26883 --- /dev/null +++ b/shierfeng-fastapi-frontend/src/api/report/palletTrace.js @@ -0,0 +1,10 @@ +import request from '@/utils/request' + +// 查询托盘追溯报表 +export function listPalletTrace(query) { + return request({ + url: '/report/pallet-trace/list', + method: 'get', + params: query + }) +} diff --git a/shierfeng-fastapi-frontend/src/api/report/return.js b/shierfeng-fastapi-frontend/src/api/report/return.js new file mode 100644 index 0000000..835f97f --- /dev/null +++ b/shierfeng-fastapi-frontend/src/api/report/return.js @@ -0,0 +1,10 @@ +import request from '@/utils/request' + +// 查询退回仓库报表 +export function listReturnReport(query) { + return request({ + url: '/report/return/list', + method: 'get', + params: query + }) +} diff --git a/shierfeng-fastapi-frontend/src/api/report/shipment.js b/shierfeng-fastapi-frontend/src/api/report/shipment.js new file mode 100644 index 0000000..7c4d8c1 --- /dev/null +++ b/shierfeng-fastapi-frontend/src/api/report/shipment.js @@ -0,0 +1,10 @@ +import request from '@/utils/request' + +// 查询发货出库报表 +export function listShipmentReport(query) { + return request({ + url: '/report/shipment/list', + method: 'get', + params: query + }) +} diff --git a/shierfeng-fastapi-frontend/src/components/PalletScanInput/index.vue b/shierfeng-fastapi-frontend/src/components/PalletScanInput/index.vue new file mode 100644 index 0000000..fe6d141 --- /dev/null +++ b/shierfeng-fastapi-frontend/src/components/PalletScanInput/index.vue @@ -0,0 +1,173 @@ + + + + + diff --git a/shierfeng-fastapi-frontend/src/utils/palletScan.js b/shierfeng-fastapi-frontend/src/utils/palletScan.js new file mode 100644 index 0000000..39088ea --- /dev/null +++ b/shierfeng-fastapi-frontend/src/utils/palletScan.js @@ -0,0 +1,17 @@ +export function normalizeScanValue(value) { + const normalizedValue = String(value ?? '').trim() + if (!normalizedValue) { + throw new Error('二维码不能为空') + } + + return normalizedValue +} + +export function isDuplicateScan(value, lastValue, scannedAt, lastScannedAt, duplicateWindowMs) { + const elapsed = scannedAt - lastScannedAt + return value === lastValue && elapsed >= 0 && elapsed <= duplicateWindowMs +} + +export function createIdempotencyKey() { + return globalThis.crypto.randomUUID() +}