功能:新增业务接口与扫码组件
This commit is contained in:
@@ -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)
|
||||||
|
})
|
||||||
@@ -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
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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'
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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'
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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'
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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'
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
// 查询产线消耗报表
|
||||||
|
export function listConsumptionReport(query) {
|
||||||
|
return request({
|
||||||
|
url: '/report/consumption/list',
|
||||||
|
method: 'get',
|
||||||
|
params: query
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
// 查询装填记录报表
|
||||||
|
export function listFillingReport(query) {
|
||||||
|
return request({
|
||||||
|
url: '/report/filling/list',
|
||||||
|
method: 'get',
|
||||||
|
params: query
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
// 查询托盘库存报表
|
||||||
|
export function listPalletInventory(query) {
|
||||||
|
return request({
|
||||||
|
url: '/report/pallet-inventory/list',
|
||||||
|
method: 'get',
|
||||||
|
params: query
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
// 查询托盘追溯报表
|
||||||
|
export function listPalletTrace(query) {
|
||||||
|
return request({
|
||||||
|
url: '/report/pallet-trace/list',
|
||||||
|
method: 'get',
|
||||||
|
params: query
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
// 查询退回仓库报表
|
||||||
|
export function listReturnReport(query) {
|
||||||
|
return request({
|
||||||
|
url: '/report/return/list',
|
||||||
|
method: 'get',
|
||||||
|
params: query
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
// 查询发货出库报表
|
||||||
|
export function listShipmentReport(query) {
|
||||||
|
return request({
|
||||||
|
url: '/report/shipment/list',
|
||||||
|
method: 'get',
|
||||||
|
params: query
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="pallet-scan-input"
|
||||||
|
:class="{
|
||||||
|
'is-loading': loading,
|
||||||
|
'is-error': errorMessage,
|
||||||
|
'is-disabled': disabled
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<el-input
|
||||||
|
ref="inputRef"
|
||||||
|
v-model="scanValue"
|
||||||
|
size="large"
|
||||||
|
clearable
|
||||||
|
:disabled="disabled"
|
||||||
|
:readonly="loading"
|
||||||
|
:aria-busy="loading"
|
||||||
|
:aria-invalid="Boolean(errorMessage)"
|
||||||
|
placeholder="请扫描或输入托盘二维码后按 Enter"
|
||||||
|
@input="clearError"
|
||||||
|
@keyup.enter="handleSubmit"
|
||||||
|
>
|
||||||
|
<template #suffix>
|
||||||
|
<el-icon v-if="loading" class="pallet-scan-input__loading is-loading">
|
||||||
|
<Loading />
|
||||||
|
</el-icon>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
<div v-if="errorMessage" class="pallet-scan-input__error" role="alert">
|
||||||
|
{{ errorMessage }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { Loading } from '@element-plus/icons-vue'
|
||||||
|
import { isDuplicateScan, normalizeScanValue } from '@/utils/palletScan'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
loading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
minLength: {
|
||||||
|
type: Number,
|
||||||
|
default: 8,
|
||||||
|
validator: (value) => Number.isInteger(value) && value > 0
|
||||||
|
},
|
||||||
|
duplicateWindowMs: {
|
||||||
|
type: Number,
|
||||||
|
default: 500,
|
||||||
|
validator: (value) => Number.isFinite(value) && value >= 0
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['scan', 'invalid', 'duplicate'])
|
||||||
|
const inputRef = ref(null)
|
||||||
|
const scanValue = ref('')
|
||||||
|
const errorMessage = ref('')
|
||||||
|
let lastScanValue = ''
|
||||||
|
let lastScanAt = 0
|
||||||
|
|
||||||
|
function focusInput() {
|
||||||
|
nextTick(() => {
|
||||||
|
if (!props.disabled) {
|
||||||
|
inputRef.value?.focus()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearError() {
|
||||||
|
errorMessage.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSubmit() {
|
||||||
|
if (props.disabled || props.loading) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let normalizedValue
|
||||||
|
try {
|
||||||
|
normalizedValue = normalizeScanValue(scanValue.value)
|
||||||
|
if (normalizedValue.length < props.minLength) {
|
||||||
|
throw new Error(`二维码长度不能少于 ${props.minLength} 个字符`)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
errorMessage.value = error.message
|
||||||
|
emit('invalid', error.message)
|
||||||
|
focusInput()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const scannedAt = Date.now()
|
||||||
|
if (isDuplicateScan(
|
||||||
|
normalizedValue,
|
||||||
|
lastScanValue,
|
||||||
|
scannedAt,
|
||||||
|
lastScanAt,
|
||||||
|
props.duplicateWindowMs
|
||||||
|
)) {
|
||||||
|
scanValue.value = ''
|
||||||
|
emit('duplicate', normalizedValue)
|
||||||
|
focusInput()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
lastScanValue = normalizedValue
|
||||||
|
lastScanAt = scannedAt
|
||||||
|
scanValue.value = ''
|
||||||
|
errorMessage.value = ''
|
||||||
|
emit('scan', normalizedValue)
|
||||||
|
focusInput()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(focusInput)
|
||||||
|
|
||||||
|
watch(() => props.loading, (loading) => {
|
||||||
|
if (!loading) {
|
||||||
|
focusInput()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.pallet-scan-input {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 48px;
|
||||||
|
|
||||||
|
:deep(.el-input__wrapper) {
|
||||||
|
min-height: 48px;
|
||||||
|
transition: box-shadow var(--el-transition-duration-fast), background-color var(--el-transition-duration-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(.is-disabled):not(.is-error):focus-within :deep(.el-input__wrapper) {
|
||||||
|
box-shadow: 0 0 0 1px var(--el-color-primary) inset;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-loading :deep(.el-input__wrapper) {
|
||||||
|
background-color: var(--el-fill-color-light);
|
||||||
|
cursor: progress;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-error :deep(.el-input__wrapper) {
|
||||||
|
box-shadow: 0 0 0 1px var(--el-color-danger) inset;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-disabled :deep(.el-input__wrapper) {
|
||||||
|
background-color: var(--el-disabled-bg-color);
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pallet-scan-input__loading {
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pallet-scan-input__error {
|
||||||
|
padding-top: 4px;
|
||||||
|
color: var(--el-color-danger);
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.pallet-scan-input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user