更新登录页及优化相关功能
This commit is contained in:
@@ -9,7 +9,7 @@
|
||||
maxlength="128"
|
||||
:focus="focusRequested"
|
||||
:disabled="disabled || loading"
|
||||
placeholder="请扫描或输入托盘二维码后按回车"
|
||||
placeholder="扫描二维码或输入托盘号后按回车"
|
||||
@confirm="handleSubmit"
|
||||
@input="clearError"
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,302 @@
|
||||
<template>
|
||||
<view v-if="canReceive" class="space-y-4">
|
||||
<view class="rounded-2xl bg-white p-4 shadow-sm">
|
||||
<text class="mb-3 block font-bold text-gray-800">收货单</text>
|
||||
<picker :range="receiptStateOptions" range-key="label" :value="receiptStateIndex" @change="changeReceiptState">
|
||||
<view class="mb-3 rounded-xl bg-gray-50 px-3 py-3 text-sm text-gray-700">{{ receiptStateOptions[receiptStateIndex].label }}</view>
|
||||
</picker>
|
||||
<input v-model="queryParams.shipmentNo" class="mb-3 rounded-xl bg-gray-50 px-3 py-3 text-sm" maxlength="64" placeholder="输入原发货单号" confirm-type="search" @confirm="handleQuery" />
|
||||
<button size="mini" class="rounded-lg bg-blue-50 text-blue-600" :disabled="loading" @click="handleQuery">查询收货</button>
|
||||
<text v-if="listError" class="mt-3 block text-xs text-red-500">{{ listError }}</text>
|
||||
<text v-if="loading" class="mt-3 block text-xs text-gray-400">加载中...</text>
|
||||
<text v-else-if="!receiptList.length" class="mt-3 block text-xs text-gray-400">暂无收货单</text>
|
||||
<view v-for="row in receiptList" :key="row.shipmentId" class="mt-3 rounded-xl border border-gray-100 p-3">
|
||||
<view class="flex items-center justify-between gap-2">
|
||||
<text class="text-sm font-medium text-gray-700">{{ row.shipmentNo }}</text>
|
||||
<text class="text-xs text-orange-600">{{ receiptStateLabel(row.receiptState) }}</text>
|
||||
</view>
|
||||
<text class="mt-2 block text-xs text-gray-500">仓库 #{{ row.sourceWarehouseId }} → 现场 #{{ row.targetSiteId }}</text>
|
||||
<text v-if="row.receiptState === 'RECEIVED'" class="mt-1 block text-xs text-gray-500">收货时间:{{ row.receivedAt || '未登记' }}</text>
|
||||
<button size="mini" class="mt-3 rounded-lg bg-orange-50 text-orange-600" @click="openReceipt(row)">{{ row.receiptState === 'PENDING' ? '核对收货' : '查看明细' }}</button>
|
||||
</view>
|
||||
<view class="mt-3 flex items-center justify-between gap-2">
|
||||
<button size="mini" :disabled="loading || queryParams.pageNum <= 1" @click="changePage(-1)">上一页</button>
|
||||
<text class="text-xs text-gray-500">第 {{ queryParams.pageNum }} 页 · 共 {{ total }} 单</text>
|
||||
<button size="mini" :disabled="loading || queryParams.pageNum * queryParams.pageSize >= total" @click="changePage(1)">下一页</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="activeShipmentId !== null" class="rounded-2xl bg-white p-4 shadow-sm">
|
||||
<view class="mb-3 flex items-center justify-between">
|
||||
<text class="font-bold text-gray-800">收货核对</text>
|
||||
<button size="mini" class="text-xs text-gray-500" @click="closeReceipt">关闭</button>
|
||||
</view>
|
||||
<text v-if="detailLoading" class="block text-xs text-gray-400">加载原单明细...</text>
|
||||
<text v-if="detailError" class="mb-3 block text-xs text-red-500">{{ detailError }}</text>
|
||||
<button v-if="detailError" size="mini" :disabled="detailLoading || confirmLoading" @click="retryDetail">重新读取详情</button>
|
||||
<template v-if="receiptDetail">
|
||||
<text class="block text-sm font-medium text-gray-700">{{ receiptDetail.shipmentNo }} · {{ receiptStateLabel(receiptDetail.receiptState) }}</text>
|
||||
<text class="mt-1 block text-xs text-gray-500">目标现场 #{{ receiptDetail.targetSiteId }}</text>
|
||||
<text v-if="receiptDetail.receiptState === 'RECEIVED'" class="mt-1 block text-xs text-gray-500">收货时间:{{ receiptDetail.receivedAt || '未登记' }}</text>
|
||||
<text v-if="receiptDetail.receiptState === 'LEGACY'" class="mt-3 block text-xs text-gray-500">历史未登记收货:仅供查询原单明细,不补造收货时间。</text>
|
||||
<view v-if="receiptDetail.receiptState === 'PENDING'" class="mt-3">
|
||||
<PalletScanInput :key="activeShipmentId" :disabled="!canScan" :loading="scanLoading" @scan="handleReceiptScan" @invalid="message => scanError = message" @duplicate="scanError = '扫码过快,请勿重复扫描'" />
|
||||
<text class="mt-2 block text-xs text-gray-500">逐托核对原标签,已核对 {{ scannedPalletIds.length }} / {{ receiptItems.length }} 托,全部核对后才可确认。</text>
|
||||
</view>
|
||||
<text v-if="scanError" class="mt-3 block text-xs text-red-500">{{ scanError }}</text>
|
||||
<text v-if="!receiptItems.length" class="mt-3 block text-xs text-red-500">原单无托盘明细,无法确认收货</text>
|
||||
<view v-for="item in receiptItems" :key="item.palletId" class="mt-3 rounded-xl bg-gray-50 p-3">
|
||||
<view class="flex items-center justify-between gap-2">
|
||||
<text class="text-sm font-medium text-gray-700">{{ item.palletCode }}</text>
|
||||
<text v-if="receiptDetail.receiptState === 'PENDING'" class="text-xs text-orange-600">{{ scannedPalletIds.includes(item.palletId) ? '已核对' : '待扫描' }}</text>
|
||||
</view>
|
||||
<text class="mt-2 block text-xs text-gray-600">品种:{{ item.strainName || '未登记' }} · 原发货 {{ item.packageQty }} 包</text>
|
||||
<text class="mt-1 block text-xs text-gray-500">批号:{{ item.batchNo || '未登记' }}</text>
|
||||
<text class="mt-1 block text-xs text-gray-500">生产日期:{{ item.productionDate || '未登记' }}</text>
|
||||
<text class="mt-1 block text-xs text-gray-500">失效日期:{{ item.expiryDate || '未登记' }}</text>
|
||||
<button v-if="receiptDetail.receiptState === 'PENDING' && scannedPalletIds.includes(item.palletId)" size="mini" class="mt-2 text-xs text-red-500" :disabled="!canScan || scanLoading" @click="removeScannedPallet(item.palletId)">撤销核对</button>
|
||||
</view>
|
||||
<text v-if="confirmError" class="mt-3 block text-xs text-red-500">{{ confirmError }}</text>
|
||||
<button v-if="receiptDetail.receiptState === 'PENDING'" class="mt-4 rounded-xl bg-orange-500 py-3 text-sm text-white disabled:bg-gray-300" :disabled="!canConfirmReceipt" :loading="confirmLoading" @click="confirmReceipt">确认收货</button>
|
||||
</template>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, getCurrentInstance, onBeforeUnmount, onMounted, reactive, ref } from 'vue'
|
||||
import { onHide, onShow, onUnload } from '@dcloudio/uni-app'
|
||||
import PalletScanInput from '@/components/business/PalletScanInput.vue'
|
||||
import { listShipmentReceipts, getShipmentReceipt, scanShipmentReceipt, confirmShipmentReceipt } from '@/api/business/shipmentReceipt'
|
||||
import { createIdempotencyKey } from '@/utils/palletScan'
|
||||
|
||||
const { proxy } = getCurrentInstance()
|
||||
const emit = defineEmits(['received'])
|
||||
const canReceive = computed(() => proxy.$auth.hasPermi('business:shipment:receive'))
|
||||
const queryParams = reactive({ pageNum: 1, pageSize: 10, receiptState: 'PENDING', shipmentNo: undefined })
|
||||
const receiptList = ref([])
|
||||
const total = ref(0)
|
||||
const loading = ref(false)
|
||||
const listError = ref('')
|
||||
const activeShipmentId = ref(null)
|
||||
const receiptDetail = ref(null)
|
||||
const detailLoading = ref(false)
|
||||
const detailError = ref('')
|
||||
const scannedPalletIds = ref([])
|
||||
const scanLoading = ref(false)
|
||||
const scanError = ref('')
|
||||
const confirmLoading = ref(false)
|
||||
const confirmError = ref('')
|
||||
const receiptAccepted = ref(false)
|
||||
let session = 0
|
||||
let listGeneration = 0
|
||||
let suspended = false
|
||||
let idempotencyKey = ''
|
||||
|
||||
const receiptItems = computed(() => receiptDetail.value?.items || [])
|
||||
const canScan = computed(() => canReceive.value && activeShipmentId.value !== null &&
|
||||
receiptDetail.value?.status === 'CONFIRMED' && receiptDetail.value?.receiptState === 'PENDING' &&
|
||||
!detailLoading.value && !detailError.value && !confirmLoading.value && !receiptAccepted.value)
|
||||
const canConfirmReceipt = computed(() => canScan.value && !scanLoading.value &&
|
||||
receiptItems.value.length > 0 && receiptItems.value.length === scannedPalletIds.value.length &&
|
||||
receiptItems.value.every(item => scannedPalletIds.value.includes(item.palletId)))
|
||||
|
||||
function receiptStateLabel(state) {
|
||||
return { PENDING: '待收货', RECEIVED: '已收货', LEGACY: '历史未登记收货' }[state] || '未登记收货'
|
||||
}
|
||||
|
||||
function errorMessage(error, fallback) {
|
||||
return error?.response?.data?.msg || error?.message || fallback
|
||||
}
|
||||
|
||||
function isCurrent(currentSession) {
|
||||
return currentSession === session && !suspended && canReceive.value
|
||||
}
|
||||
|
||||
async function loadReceipts() {
|
||||
if (!canReceive.value || suspended) return
|
||||
const generation = ++listGeneration
|
||||
loading.value = true
|
||||
listError.value = ''
|
||||
try {
|
||||
const response = await listShipmentReceipts({ ...queryParams })
|
||||
if (generation !== listGeneration || suspended || !canReceive.value) return
|
||||
receiptList.value = Array.isArray(response.rows) ? response.rows : []
|
||||
total.value = response.total || 0
|
||||
} catch (error) {
|
||||
if (generation !== listGeneration || suspended || !canReceive.value) return
|
||||
receiptList.value = []
|
||||
total.value = 0
|
||||
listError.value = errorMessage(error, '收货列表加载失败,请重试')
|
||||
} finally {
|
||||
if (generation === listGeneration) loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleQuery() {
|
||||
queryParams.pageNum = 1
|
||||
return loadReceipts()
|
||||
}
|
||||
|
||||
function closeReceipt() {
|
||||
// 会话版本同时隔离详情、扫码、确认响应,旧请求不能写入新单据。
|
||||
session += 1
|
||||
activeShipmentId.value = null
|
||||
receiptDetail.value = null
|
||||
scannedPalletIds.value = []
|
||||
detailLoading.value = false
|
||||
scanLoading.value = false
|
||||
confirmLoading.value = false
|
||||
receiptAccepted.value = false
|
||||
detailError.value = ''
|
||||
scanError.value = ''
|
||||
confirmError.value = ''
|
||||
idempotencyKey = ''
|
||||
}
|
||||
|
||||
async function loadReceiptDetail(shipmentId, currentSession) {
|
||||
detailLoading.value = true
|
||||
detailError.value = ''
|
||||
try {
|
||||
const response = await getShipmentReceipt(shipmentId)
|
||||
if (!isCurrent(currentSession)) return
|
||||
if (!response.data || String(response.data.shipmentId) !== String(shipmentId)) {
|
||||
throw new Error('收货详情与当前单据不一致,请重新打开')
|
||||
}
|
||||
receiptDetail.value = response.data
|
||||
} catch (error) {
|
||||
if (!isCurrent(currentSession)) return
|
||||
detailError.value = errorMessage(error, '收货详情加载失败,请重试')
|
||||
} finally {
|
||||
if (isCurrent(currentSession)) detailLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function openReceipt(row) {
|
||||
if (!canReceive.value || suspended || row.shipmentId == null) return
|
||||
closeReceipt()
|
||||
activeShipmentId.value = row.shipmentId
|
||||
await loadReceiptDetail(row.shipmentId, session)
|
||||
}
|
||||
|
||||
function retryDetail() {
|
||||
if (activeShipmentId.value === null || detailLoading.value || confirmLoading.value) return
|
||||
return loadReceiptDetail(activeShipmentId.value, session)
|
||||
}
|
||||
|
||||
async function handleReceiptScan(identifier) {
|
||||
if (!canScan.value || scanLoading.value) return
|
||||
const value = String(identifier ?? '').trim()
|
||||
if (!value) {
|
||||
scanError.value = '请扫描二维码或输入托盘号'
|
||||
return
|
||||
}
|
||||
const currentSession = session
|
||||
scanLoading.value = true
|
||||
scanError.value = ''
|
||||
try {
|
||||
const response = await scanShipmentReceipt(activeShipmentId.value, value)
|
||||
if (!isCurrent(currentSession)) return
|
||||
const scanned = response.data
|
||||
const original = receiptItems.value.find(item => String(item.palletId) === String(scanned?.palletId))
|
||||
if (!original || String(original.activeFillingId) !== String(scanned?.activeFillingId)) {
|
||||
scanError.value = '该托盘或装填记录不属于当前发货单'
|
||||
return
|
||||
}
|
||||
if (scannedPalletIds.value.includes(original.palletId)) {
|
||||
scanError.value = '该托盘已核对,请勿重复扫描'
|
||||
return
|
||||
}
|
||||
// 仅登记实际扫码通过的 ID;品种、批号、日期与包数始终展示原单快照。
|
||||
scannedPalletIds.value.push(original.palletId)
|
||||
idempotencyKey = ''
|
||||
confirmError.value = ''
|
||||
} catch (error) {
|
||||
if (isCurrent(currentSession)) scanError.value = errorMessage(error, '托盘核对失败,请重试')
|
||||
} finally {
|
||||
if (isCurrent(currentSession)) scanLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function removeScannedPallet(palletId) {
|
||||
if (!canScan.value || scanLoading.value) return
|
||||
if (!scannedPalletIds.value.includes(palletId)) return
|
||||
scannedPalletIds.value = scannedPalletIds.value.filter(id => id !== palletId)
|
||||
idempotencyKey = ''
|
||||
confirmError.value = ''
|
||||
}
|
||||
|
||||
async function confirmReceipt() {
|
||||
if (!canReceive.value || confirmLoading.value || receiptAccepted.value) return
|
||||
if (!canConfirmReceipt.value) {
|
||||
confirmError.value = '请逐托扫码核对全部明细后再确认收货'
|
||||
return
|
||||
}
|
||||
const currentSession = session
|
||||
const shipmentId = activeShipmentId.value
|
||||
confirmLoading.value = true
|
||||
confirmError.value = ''
|
||||
try {
|
||||
// 失败保留同一键;增删核对集合时作废,下一次提交生成新键。
|
||||
if (!idempotencyKey) idempotencyKey = createIdempotencyKey()
|
||||
const response = await confirmShipmentReceipt(shipmentId, {
|
||||
scannedPalletIds: [...scannedPalletIds.value], idempotencyKey
|
||||
})
|
||||
if (!isCurrent(currentSession)) return
|
||||
receiptAccepted.value = true
|
||||
proxy.$modal.msgSuccess(response.msg || '收货成功')
|
||||
emit('received')
|
||||
// 不推算余量,也不伪造已收状态/时间。刷新失败仍锁定本次已接受的确认。
|
||||
await Promise.all([loadReceiptDetail(shipmentId, currentSession), loadReceipts()])
|
||||
} catch (error) {
|
||||
if (isCurrent(currentSession)) {
|
||||
confirmError.value = errorMessage(error, '收货确认失败') + ';未修改核对集合时重试使用同一幂等键'
|
||||
}
|
||||
} finally {
|
||||
if (isCurrent(currentSession)) confirmLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function suspendPanel() {
|
||||
suspended = true
|
||||
listGeneration += 1
|
||||
loading.value = false
|
||||
closeReceipt()
|
||||
}
|
||||
|
||||
function resumePanel() {
|
||||
if (!suspended) return
|
||||
suspended = false
|
||||
loadReceipts()
|
||||
}
|
||||
|
||||
onMounted(loadReceipts)
|
||||
onBeforeUnmount(suspendPanel)
|
||||
|
||||
const receiptStateOptions = [
|
||||
{ label: '待收货', value: 'PENDING' },
|
||||
{ label: '已收货', value: 'RECEIVED' },
|
||||
{ label: '历史未登记收货', value: 'LEGACY' },
|
||||
{ label: '全部', value: undefined }
|
||||
]
|
||||
const receiptStateIndex = computed(() => receiptStateOptions.findIndex(item => item.value === queryParams.receiptState))
|
||||
|
||||
function changeReceiptState(event) {
|
||||
const option = receiptStateOptions[Number(event.detail.value)]
|
||||
if (!option) return
|
||||
queryParams.receiptState = option.value
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
function changePage(offset) {
|
||||
const page = queryParams.pageNum + offset
|
||||
if (loading.value || page < 1 || (page > 1 && (page - 1) * queryParams.pageSize >= total.value)) return
|
||||
queryParams.pageNum = page
|
||||
loadReceipts()
|
||||
}
|
||||
|
||||
onShow(resumePanel)
|
||||
onHide(suspendPanel)
|
||||
onUnload(suspendPanel)
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user