469 lines
15 KiB
Vue
469 lines
15 KiB
Vue
<template>
|
|
<div class="app-container">
|
|
<el-form ref="queryRef" :model="queryParams" :inline="true">
|
|
<el-form-item label="装填单号" prop="fillingNo">
|
|
<el-input v-model="queryParams.fillingNo" placeholder="请输入装填单号" clearable maxlength="64" @keyup.enter="handleQuery" />
|
|
</el-form-item>
|
|
<el-form-item label="托盘编号" prop="palletCode">
|
|
<el-input v-model="queryParams.palletCode" placeholder="请输入托盘编号" clearable maxlength="64" @keyup.enter="handleQuery" />
|
|
</el-form-item>
|
|
<el-form-item label="生产批次" prop="batchNo">
|
|
<el-input v-model="queryParams.batchNo" placeholder="请输入生产批次" clearable maxlength="64" @keyup.enter="handleQuery" />
|
|
</el-form-item>
|
|
<el-form-item>
|
|
<el-button type="primary" icon="Search" :disabled="operationPending" @click="handleQuery" v-hasPermi="['business:filling:list']">搜索</el-button>
|
|
<el-button icon="Refresh" :disabled="operationPending" @click="resetQuery" v-hasPermi="['business:filling:list']">重置</el-button>
|
|
</el-form-item>
|
|
</el-form>
|
|
|
|
<el-row :gutter="10" class="mb8">
|
|
<el-col :span="1.5">
|
|
<el-button
|
|
v-if="canAddFilling"
|
|
class="business-action"
|
|
type="primary"
|
|
plain
|
|
icon="Plus"
|
|
:disabled="operationPending"
|
|
@click="handleOpenFilling"
|
|
v-hasPermi="['business:filling:add']"
|
|
>托盘装填</el-button>
|
|
</el-col>
|
|
</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">
|
|
<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>
|
|
</el-table-column>
|
|
<el-table-column label="仓库ID" prop="warehouseId" width="110" />
|
|
<el-table-column label="菌种" min-width="160">
|
|
<template #default="scope">{{ scope.row.strainNameSnapshot || scope.row.strainCodeSnapshot || `#${scope.row.strainId}` }}</template>
|
|
</el-table-column>
|
|
<el-table-column label="生产批次" prop="batchNo" min-width="130" :show-overflow-tooltip="true" />
|
|
<el-table-column label="包装规格" prop="packageSpec" min-width="120" :show-overflow-tooltip="true" />
|
|
<el-table-column label="初始包数" prop="initialPackages" width="100" align="right" />
|
|
<el-table-column label="操作时间" prop="operatedAt" width="180">
|
|
<template #default="scope">{{ parseTime(scope.row.operatedAt) }}</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="open"
|
|
title="托盘装填"
|
|
width="min(680px, 94vw)"
|
|
append-to-body
|
|
:close-on-click-modal="!operationPending"
|
|
:close-on-press-escape="!operationPending"
|
|
:show-close="!operationPending"
|
|
:before-close="handleDialogBeforeClose"
|
|
>
|
|
<el-alert v-if="optionError" :title="optionError" type="error" :closable="false" show-icon class="mb8" />
|
|
<el-alert v-if="scanError" :title="scanError" type="error" :closable="false" show-icon class="mb8" />
|
|
<el-alert v-if="formError" :title="formError" type="error" :closable="false" show-icon class="mb8" />
|
|
|
|
<el-form ref="fillingRef" :model="form" :rules="rules" label-width="96px">
|
|
<el-form-item label="扫描托盘">
|
|
<pallet-scan-input
|
|
:disabled="operationPending || optionLoading"
|
|
:loading="scanLoading"
|
|
@scan="handleFillingScan"
|
|
@invalid="handleScanInvalid"
|
|
@duplicate="handleScanDuplicate"
|
|
/>
|
|
</el-form-item>
|
|
|
|
<el-descriptions v-if="scannedPallet" :column="2" border class="scan-summary">
|
|
<el-descriptions-item label="托盘">{{ scannedPallet.palletCode || `#${scannedPallet.palletId}` }}</el-descriptions-item>
|
|
<el-descriptions-item label="仓库">#{{ scannedPallet.warehouseId }}</el-descriptions-item>
|
|
<el-descriptions-item label="流转状态">{{ scannedPallet.flowState }}</el-descriptions-item>
|
|
<el-descriptions-item label="托盘容量">{{ scannedPallet.capacityPackages }} 包</el-descriptions-item>
|
|
</el-descriptions>
|
|
<el-empty v-else description="请先扫描待装填托盘" :image-size="72" />
|
|
|
|
<el-form-item label="菌种" prop="strainId">
|
|
<el-select
|
|
v-model="form.strainId"
|
|
class="business-control"
|
|
placeholder="请选择菌种"
|
|
filterable
|
|
style="width: 100%"
|
|
:loading="optionLoading"
|
|
:disabled="operationPending || scanLoading || optionLoading"
|
|
>
|
|
<el-option
|
|
v-for="option in strainOptions"
|
|
:key="option.id"
|
|
:label="option.name || `#${option.id}`"
|
|
:value="option.id"
|
|
/>
|
|
</el-select>
|
|
</el-form-item>
|
|
<el-form-item label="生产批次" prop="batchNo">
|
|
<el-input
|
|
v-model="form.batchNo"
|
|
class="business-control"
|
|
maxlength="64"
|
|
show-word-limit
|
|
placeholder="请输入生产批次"
|
|
:disabled="operationPending || scanLoading"
|
|
/>
|
|
</el-form-item>
|
|
<el-form-item label="包装规格" prop="packageSpec">
|
|
<el-input
|
|
v-model="form.packageSpec"
|
|
class="business-control"
|
|
maxlength="64"
|
|
show-word-limit
|
|
placeholder="请输入包装规格"
|
|
:disabled="operationPending || scanLoading"
|
|
/>
|
|
</el-form-item>
|
|
<el-form-item label="初始包数" prop="initialPackages">
|
|
<el-input-number
|
|
v-model="form.initialPackages"
|
|
class="business-control"
|
|
:min="1"
|
|
:precision="0"
|
|
controls-position="right"
|
|
style="width: 100%"
|
|
:disabled="operationPending || scanLoading"
|
|
/>
|
|
</el-form-item>
|
|
</el-form>
|
|
|
|
<template #footer>
|
|
<div class="dialog-footer">
|
|
<el-button
|
|
class="business-action"
|
|
type="primary"
|
|
:loading="submitLoading"
|
|
:disabled="operationPending || scanLoading || optionLoading || !scannedPallet"
|
|
@click="submitFilling"
|
|
v-hasPermi="['business:filling:add']"
|
|
>确认装填</el-button>
|
|
<el-button class="business-action" :disabled="operationPending" @click="cancelFilling">取消</el-button>
|
|
</div>
|
|
</template>
|
|
</el-dialog>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup name="BusinessFilling">
|
|
import PalletScanInput from '@/components/PalletScanInput'
|
|
import { createIdempotencyKey } from '@/utils/palletScan'
|
|
import { addFilling, getFillingStrainOptions, listFilling, scanFillingPallet } from '@/api/business/filling'
|
|
|
|
const { proxy } = getCurrentInstance()
|
|
const fillingList = ref([])
|
|
const strainOptions = ref([])
|
|
const scannedPallet = ref(null)
|
|
const loading = ref(false)
|
|
const optionLoading = ref(false)
|
|
const scanLoading = ref(false)
|
|
const submitLoading = ref(false)
|
|
const loadError = ref('')
|
|
const optionError = ref('')
|
|
const scanError = ref('')
|
|
const formError = ref('')
|
|
const open = ref(false)
|
|
const total = ref(0)
|
|
const fillingIdempotencyKey = ref('')
|
|
const canAddFilling = computed(() => proxy.$auth.hasPermiAnd(['business:filling:query', 'business:filling:add']))
|
|
const operationPending = computed(() => submitLoading.value)
|
|
let listGeneration = 0
|
|
let optionGeneration = 0
|
|
let scanGeneration = 0
|
|
let dialogSession = 0
|
|
|
|
function validateRequiredText(rule, value, callback) {
|
|
const labels = { batchNo: '生产批次', packageSpec: '包装规格' }
|
|
if (typeof value !== 'string' || value.trim() === '') {
|
|
callback(new Error(`${labels[rule.field]}不能为空`))
|
|
return
|
|
}
|
|
callback()
|
|
}
|
|
|
|
function validatePositiveInteger(rule, value, callback) {
|
|
if (!Number.isInteger(value) || value <= 0) {
|
|
callback(new Error('初始包数必须为正整数'))
|
|
return
|
|
}
|
|
callback()
|
|
}
|
|
|
|
const data = reactive({
|
|
form: { qrToken: '', warehouseId: undefined, strainId: undefined, batchNo: '', packageSpec: '', initialPackages: undefined },
|
|
queryParams: { pageNum: 1, pageSize: 10, fillingNo: undefined, palletCode: undefined, batchNo: undefined },
|
|
rules: {
|
|
strainId: [{ required: true, message: '请选择菌种', trigger: 'change' }],
|
|
batchNo: [{ required: true, validator: validateRequiredText, trigger: 'blur' }],
|
|
packageSpec: [{ required: true, validator: validateRequiredText, trigger: 'blur' }],
|
|
initialPackages: [{ required: true, validator: validatePositiveInteger, trigger: 'blur' }]
|
|
}
|
|
})
|
|
const { form, queryParams, rules } = toRefs(data)
|
|
|
|
function isCurrentListRequest(listContext) {
|
|
return listContext.generation === listGeneration
|
|
}
|
|
|
|
function getList() {
|
|
const listContext = {
|
|
generation: ++listGeneration,
|
|
query: { ...queryParams.value }
|
|
};
|
|
loading.value = true
|
|
loadError.value = ''
|
|
listFilling(listContext.query).then((response) => {
|
|
if (!isCurrentListRequest(listContext)) {
|
|
return;
|
|
}
|
|
fillingList.value = response.rows
|
|
total.value = response.total
|
|
}).catch(() => {
|
|
if (!isCurrentListRequest(listContext)) {
|
|
return;
|
|
}
|
|
fillingList.value = []
|
|
total.value = 0
|
|
loadError.value = '装填记录加载失败,请稍后重试'
|
|
}).finally(() => {
|
|
if (!isCurrentListRequest(listContext)) {
|
|
return;
|
|
}
|
|
loading.value = false
|
|
})
|
|
}
|
|
|
|
function invalidateOptionRequest() {
|
|
optionGeneration += 1
|
|
optionLoading.value = false
|
|
}
|
|
|
|
function isCurrentOptionRequest(optionContext) {
|
|
return optionContext.generation === optionGeneration && optionContext.dialogSession === dialogSession
|
|
}
|
|
|
|
function loadStrainOptions() {
|
|
const optionContext = { generation: ++optionGeneration, dialogSession }
|
|
optionLoading.value = true
|
|
optionError.value = ''
|
|
getFillingStrainOptions().then((response) => {
|
|
if (!isCurrentOptionRequest(optionContext)) return
|
|
strainOptions.value = Array.isArray(response.data) ? response.data : []
|
|
}).catch(() => {
|
|
if (!isCurrentOptionRequest(optionContext)) return
|
|
strainOptions.value = []
|
|
optionError.value = '菌种选项加载失败,请稍后重试'
|
|
}).finally(() => {
|
|
if (!isCurrentOptionRequest(optionContext)) return
|
|
optionLoading.value = false
|
|
})
|
|
}
|
|
|
|
function invalidateScanRequest() {
|
|
scanGeneration += 1
|
|
scanLoading.value = false
|
|
}
|
|
|
|
function isCurrentScanRequest(scanContext) {
|
|
return scanContext.generation === scanGeneration && scanContext.dialogSession === dialogSession
|
|
}
|
|
|
|
function clearFillingIdempotencyKey() {
|
|
fillingIdempotencyKey.value = ''
|
|
}
|
|
|
|
function clearFillingPayload() {
|
|
clearFillingIdempotencyKey()
|
|
scannedPallet.value = null
|
|
form.value = {
|
|
qrToken: '',
|
|
warehouseId: undefined,
|
|
strainId: undefined,
|
|
batchNo: '',
|
|
packageSpec: '',
|
|
initialPackages: undefined
|
|
}
|
|
}
|
|
|
|
function ensureFillingIdempotencyKey() {
|
|
if (!fillingIdempotencyKey.value) {
|
|
fillingIdempotencyKey.value = createIdempotencyKey()
|
|
}
|
|
return fillingIdempotencyKey.value
|
|
}
|
|
|
|
function resetFillingOperation() {
|
|
dialogSession += 1
|
|
invalidateOptionRequest()
|
|
invalidateScanRequest()
|
|
clearFillingPayload()
|
|
optionError.value = ''
|
|
scanError.value = ''
|
|
formError.value = ''
|
|
proxy.resetForm('fillingRef')
|
|
}
|
|
|
|
function handleOpenFilling() {
|
|
if (!canAddFilling.value || operationPending.value) return
|
|
resetFillingOperation()
|
|
open.value = true
|
|
loadStrainOptions()
|
|
}
|
|
|
|
function cancelFilling() {
|
|
if (operationPending.value) return
|
|
clearFillingIdempotencyKey()
|
|
open.value = false
|
|
resetFillingOperation()
|
|
}
|
|
|
|
function handleDialogBeforeClose(done) {
|
|
if (operationPending.value) return
|
|
resetFillingOperation()
|
|
done()
|
|
}
|
|
|
|
function handleQuery() {
|
|
queryParams.value.pageNum = 1
|
|
getList()
|
|
}
|
|
|
|
function resetQuery() {
|
|
proxy.resetForm('queryRef')
|
|
handleQuery()
|
|
}
|
|
|
|
function handleFillingScan(qrToken) {
|
|
if (operationPending.value || scanLoading.value) return
|
|
clearFillingPayload()
|
|
const scanContext = { generation: ++scanGeneration, dialogSession, qrToken }
|
|
scanLoading.value = true
|
|
scanError.value = ''
|
|
scanFillingPallet(qrToken).then((response) => {
|
|
if (!isCurrentScanRequest(scanContext)) return
|
|
const pallet = response.data
|
|
if (pallet.flowState !== 'EMPTY_IN_WAREHOUSE') {
|
|
clearFillingPayload()
|
|
scanError.value = '该托盘当前不可装填'
|
|
return
|
|
}
|
|
scannedPallet.value = {
|
|
palletId: pallet.palletId,
|
|
palletCode: pallet.palletCode || `#${pallet.palletId}`,
|
|
warehouseId: pallet.warehouseId,
|
|
flowState: pallet.flowState,
|
|
capacityPackages: pallet.capacityPackages
|
|
}
|
|
form.value.qrToken = qrToken
|
|
form.value.warehouseId = pallet.warehouseId
|
|
}).catch(() => {
|
|
if (!isCurrentScanRequest(scanContext)) return
|
|
clearFillingPayload()
|
|
scanError.value = '托盘扫码校验失败,请检查后重试'
|
|
}).finally(() => {
|
|
if (!isCurrentScanRequest(scanContext)) return
|
|
scanLoading.value = false
|
|
})
|
|
}
|
|
|
|
function handleScanInvalid(message) {
|
|
clearFillingPayload()
|
|
scanError.value = message
|
|
}
|
|
|
|
function handleScanDuplicate() {
|
|
proxy.$modal.msgWarning('扫码过快,请勿重复扫描')
|
|
}
|
|
|
|
function submitFilling() {
|
|
if (operationPending.value || scanLoading.value || optionLoading.value) return
|
|
submitLoading.value = true
|
|
proxy.$refs.fillingRef.validate((valid) => {
|
|
if (!valid) {
|
|
submitLoading.value = false
|
|
return
|
|
}
|
|
if (!scannedPallet.value || !form.value.qrToken || !form.value.warehouseId) {
|
|
formError.value = '请先扫描待装填托盘'
|
|
submitLoading.value = false
|
|
return
|
|
}
|
|
const idempotencyKey = ensureFillingIdempotencyKey()
|
|
formError.value = ''
|
|
addFilling({
|
|
qrToken: form.value.qrToken,
|
|
warehouseId: form.value.warehouseId,
|
|
strainId: form.value.strainId,
|
|
batchNo: form.value.batchNo.trim(),
|
|
packageSpec: form.value.packageSpec.trim(),
|
|
initialPackages: form.value.initialPackages,
|
|
idempotencyKey: idempotencyKey
|
|
}).then((response) => {
|
|
clearFillingIdempotencyKey()
|
|
proxy.$modal.msgSuccess(response.msg || '托盘装填成功')
|
|
open.value = false
|
|
resetFillingOperation()
|
|
getList()
|
|
}).catch(() => {
|
|
formError.value = '装填提交失败;修正业务输入会生成新幂等键,直接重试将复用原键'
|
|
}).finally(() => {
|
|
submitLoading.value = false
|
|
})
|
|
})
|
|
}
|
|
|
|
watch(
|
|
() => [form.value.qrToken, form.value.warehouseId, form.value.strainId, form.value.batchNo, form.value.packageSpec, form.value.initialPackages],
|
|
() => {
|
|
if (!submitLoading.value) clearFillingIdempotencyKey()
|
|
}
|
|
)
|
|
|
|
getList();
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
.scan-summary {
|
|
margin: 0 0 18px 96px;
|
|
}
|
|
|
|
@media (max-width: 767px) {
|
|
:deep(.el-dialog) {
|
|
width: 94vw !important;
|
|
}
|
|
|
|
.scan-summary {
|
|
margin-left: 0;
|
|
}
|
|
|
|
:deep(.business-control .el-input__wrapper),
|
|
:deep(.business-action) {
|
|
min-height: 48px;
|
|
}
|
|
|
|
:deep(.el-form-item__label) {
|
|
float: none;
|
|
display: block;
|
|
width: 100% !important;
|
|
text-align: left;
|
|
}
|
|
|
|
:deep(.el-form-item__content) {
|
|
margin-left: 0 !important;
|
|
}
|
|
}
|
|
</style>
|