Files
ShiErFeng/shierfeng-fastapi-frontend/src/views/business/filling/index.vue
T

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>