feat:产品入库-库区添加扫码录入功能

master
黄伟杰 13 hours ago
parent d7cb0d2a8b
commit f1b6d2d3ec

@ -80,3 +80,10 @@ export function getWarehouseAreaSimpleList(warehouseId) {
params: { warehouseId }
})
}
export function getWarehouseArea(id) {
return request({
url: '/admin-api/erp/warehouse-area/get',
method: 'get',
params: { id }
})
}

@ -1585,6 +1585,7 @@ export default {
enterPackageCount: 'Enter packages',
selectWarehouse: 'Select warehouse',
selectArea: 'Select area',
selectAreaTitle: 'Select Area',
selectUnit: 'Select unit',
selectWarehouseFirst: 'Select warehouse first',
emptyArea: 'No areas',

@ -1551,7 +1551,7 @@ export default {
selectTaskFirst: '请先选择任务单',
selectProductFirst: '请先选择产品',
emptyTaskProducts: '当前任务单暂无产品',
completePalletInfo: '请完善托盘仓库/库/包数',
completePalletInfo: '请完善托盘仓库/库/包数',
productAdded: '已添加产品',
taskProductLoadFailed: '任务产品加载失败',
workOrderNo: '工单号',
@ -1588,6 +1588,7 @@ export default {
enterPackageCount: '请输入包数',
selectWarehouse: '请选择仓库',
selectArea: '请选择库区',
selectAreaTitle: '选择库区',
selectUnit: '请选择单位',
selectWarehouseFirst: '请先选择仓库',
emptyArea: '暂无库区',
@ -1779,7 +1780,7 @@ export default {
selectPallet: '请选择托盘',
selectPalletFirst: '请先选择托盘',
selectProductFirst: '请先选择产品',
completePalletInfo: '请完善托盘仓库/库/包数',
completePalletInfo: '请完善托盘仓库/库/包数',
productAdded: '已添加产品',
code: '编码',
packagingScheme: '包装方案',

@ -90,10 +90,10 @@
<view v-if="itemList.length" class="item-list">
<view v-for="(item, idx) in itemList" :key="idx" class="item-card" @click="editItem(idx)">
<view class="image-box">
<!-- <view class="image-box">
<image v-if="getItemImage(item)" :src="getItemImage(item)" class="item-image" mode="aspectFill" />
<uni-icons v-else type="image" size="34" color="#cbd5e1"></uni-icons>
</view>
</view> -->
<view class="item-info">
<view class="item-header">
<text class="item-name">{{ textValue(item.productName) }}</text>

@ -152,34 +152,52 @@
<uni-icons type="closeempty" size="18" color="#ef4444"></uni-icons>
</view>
</view>
<view class="pallet-loc">
<view class="pallet-field-list">
<view class="loc-cell">
<text class="loc-label">{{ t('productInbound.warehouse') }}</text>
<picker mode="selector" :range="warehouseOptions" range-key="label" :value="getWarehouseIndex(pallet.warehouseId)" @change="onPalletWarehouseChange($event, index)">
<view class="loc-picker">
<view class="loc-picker pallet-field-control">
<text :class="pallet.warehouseId ? 'loc-value' : 'loc-placeholder'">{{ pallet.warehouseId ? getWarehouseName(pallet.warehouseId) : t('productInbound.choose') }}</text>
<uni-icons type="bottom" size="14" color="#9ca3af"></uni-icons>
</view>
</picker>
</view>
<view class="loc-cell">
<text class="loc-label">{{ t('productInbound.location') }}</text>
<picker mode="selector" :range="getAreaOptions(pallet.warehouseId)" range-key="label" :value="getAreaIndex(pallet.warehouseId, pallet.areaId)" :disabled="!pallet.warehouseId" @change="onPalletAreaChange($event, index)">
<view class="loc-picker">
<text :class="pallet.areaId ? 'loc-value' : 'loc-placeholder'">{{ pallet.areaId ? getAreaName(pallet.warehouseId, pallet.areaId) : t('productInbound.choose') }}</text>
<uni-icons type="bottom" size="14" color="#9ca3af"></uni-icons>
<text class="loc-label">{{ t('productInbound.area') }}</text>
<view class="task-product-search-row area-select-row">
<view class="scan-input-wrap">
<input
v-model="pallet._areaScanInput"
class="task-product-scan-input"
type="text"
:placeholder="getPalletAreaDisplayName(pallet) || t('productInbound.selectArea')"
confirm-type="done"
@input="onPalletAreaScanInput($event, index)"
@confirm="onPalletAreaScanConfirm(index)"
/>
<view class="scan-input-icon">
<uni-icons type="scan" size="20" color="#9ca3af"></uni-icons>
</view>
</view>
</picker>
<view v-if="pallet.warehouseId && getAreaOptions(pallet.warehouseId).length" class="area-select-picker">
<picker mode="selector" :range="getAreaOptions(pallet.warehouseId)" range-key="label" :value="getAreaIndex(pallet.warehouseId, pallet.areaId)" @change="onPalletAreaChange($event, index)">
<view class="task-product-select-field area-select-button">
<text class="task-product-select-text">{{ t('productInbound.selectAreaTitle') }}</text>
</view>
</picker>
</view>
<view v-else class="task-product-select-field area-select-button disabled" @click="onAreaPickerUnavailable(index)">
<text class="task-product-select-text">{{ t('productInbound.selectAreaTitle') }}</text>
</view>
</view>
</view>
</view>
<view class="pallet-count-row">
<view class="count-cell">
<view class="loc-cell">
<text class="count-label">{{ t('productInbound.packageCount') }}</text>
<input class="count-input" type="number" :value="pallet.packageCount" @input="onPalletCountInput($event, index)" placeholder="0" />
<input class="count-input pallet-field-control" type="number" :value="pallet.packageCount" @input="onPalletCountInput($event, index)" placeholder="0" />
</view>
<view class="count-cell">
<view class="pallet-piece-row">
<text class="count-label">{{ t('productInbound.pieceCount') }}</text>
<text class="count-value">{{ getPalletItemCount(pallet) }}</text>
<text class="pallet-piece-value">{{ getPalletItemCount(pallet) }}</text>
</view>
</view>
</view>
@ -214,7 +232,7 @@ import { useI18n } from 'vue-i18n'
import NavBar from '@/components/common/NavBar.vue'
import { getPallet, getTaskDefaultPackagingSchemes } from '@/api/mes/productInbound'
import { getProduct } from '@/api/erp/productInfo'
import { getWarehouseAreaSimpleList, getWarehouseSimpleList } from '@/api/mes/moldget'
import { getWarehouseArea, getWarehouseAreaSimpleList, getWarehouseSimpleList } from '@/api/mes/moldget'
const { t } = useI18n()
const product = ref({})
@ -234,10 +252,12 @@ const warehouseAreaMap = ref({})
const taskProductList = ref([])
const PRODUCT_CATEGORY_TYPE = 1
const WAREHOUSE_CATEGORY_TYPE = 1
const SCAN_AUTO_SEARCH_DELAY = 300
let taskProductScanTimer = null
let productScanTimer = null
let palletScanTimer = null
const palletAreaScanTimers = {}
const productName = computed(() => selectedTaskProduct.value?.productName || product.value.name || '')
const productId = computed(() => selectedTaskProduct.value?.productId || product.value.id)
@ -329,7 +349,8 @@ async function hydrateEditItem(item) {
}
selectedPallets.value = normalizeEditPallets(item)
await loadAreasForPallets(selectedPallets.value)
}function textValue(v) {
}
function textValue(v) {
if (v === 0) return '0'
if (v == null) return '-'
const s = String(v).trim()
@ -352,6 +373,12 @@ function getAreaName(warehouseId, areaId) {
function getAreaOptions(warehouseId) {
return warehouseAreaMap.value[String(warehouseId)] || []
}
function getWarehouseAreaLabel(area) {
return area?.areaName || area?.name || area?.areaCode || String(area?.id || '')
}
function getPalletAreaDisplayName(pallet) {
return getAreaName(pallet?.warehouseId, pallet?.areaId) || pallet?.areaName || pallet?.warehouseAreaName || ''
}
function getWarehouseIndex(id) {
const i = warehouseOptions.value.findIndex((item) => String(item.value) === String(id))
return i >= 0 ? i : 0
@ -370,13 +397,31 @@ async function onPalletWarehouseChange(e, index) {
if (!opt) return
selectedPallets.value[index].warehouseId = opt.value
selectedPallets.value[index].areaId = null
selectedPallets.value[index].areaName = ''
selectedPallets.value[index].warehouseAreaName = ''
selectedPallets.value[index]._areaScanInput = ''
await loadAreasForWarehouse(opt.value)
}
function applySelectedArea(index, opt) {
if (!opt || !selectedPallets.value[index]) return
selectedPallets.value[index].areaId = opt.value
selectedPallets.value[index].areaName = opt.label
selectedPallets.value[index]._areaScanInput = ''
}
function onPalletAreaChange(e, index) {
const areas = getAreaOptions(selectedPallets.value[index].warehouseId)
const opt = areas[Number(e.detail.value)]
if (!opt) return
selectedPallets.value[index].areaId = opt.value
applySelectedArea(index, areas[Number(e.detail.value)])
}
function onAreaPickerUnavailable(index) {
const pallet = selectedPallets.value[index]
if (!pallet?.warehouseId) {
uni.showToast({ title: '\u8bf7\u5148\u9009\u62e9\u4ed3\u5e93', icon: 'none' })
return
}
uni.showToast({ title: t('productInbound.emptyArea'), icon: 'none' })
}
function onSelectAreaWithoutWarehouse() {
uni.showToast({ title: '\u8bf7\u5148\u9009\u62e9\u4ed3\u5e93', icon: 'none' })
}
function onPalletCountInput(e, index) {
const v = Number(e.detail.value)
@ -424,6 +469,11 @@ function getPalletScanCode(value) {
const match = text.match(/^PALLET[-:]?\s*(.+)$/i)
return match ? match[1].trim() : ''
}
function getWarehouseAreaScanCode(value) {
const text = String(value || '').trim()
const match = text.match(/^WAREHOUSE_AREA[-:]?\s*(.+)$/i)
return match ? match[1].trim() : ''
}
function getProductIdValue(item) {
return item?.id || item?.productId
}
@ -603,6 +653,88 @@ async function onPalletScanConfirm() {
uni.showToast({ title: t('productInbound.loadFailed'), icon: 'none' })
}
}
function getPalletAreaTimerKey(index) {
const pallet = selectedPallets.value[index]
return String(pallet?.id || index)
}
function schedulePalletAreaScanSearch(value, index) {
if (!getWarehouseAreaScanCode(value)) return
const key = getPalletAreaTimerKey(index)
if (palletAreaScanTimers[key]) clearTimeout(palletAreaScanTimers[key])
palletAreaScanTimers[key] = setTimeout(() => {
palletAreaScanTimers[key] = null
onPalletAreaScanConfirm(index)
}, SCAN_AUTO_SEARCH_DELAY)
}
function onPalletAreaScanInput(e, index) {
const pallet = selectedPallets.value[index]
if (!pallet) return
const value = e?.detail?.value ?? pallet._areaScanInput ?? ''
pallet._areaScanInput = value
schedulePalletAreaScanSearch(value, index)
}
async function findWarehouseAreaByScanCode(scanCode) {
const id = String(scanCode || '').trim()
if (!/^\d+$/.test(id)) return null
const res = await getWarehouseArea(id)
const detail = res?.data || res
return detail?.id ? detail : null
}
function cacheWarehouseArea(area) {
if (!area?.id || !area?.warehouseId) return
const key = String(area.warehouseId)
const current = warehouseAreaMap.value[key] || []
const nextItem = { value: area.id, label: getWarehouseAreaLabel(area) }
const exists = current.some((item) => String(item.value) === String(area.id))
warehouseAreaMap.value = {
...warehouseAreaMap.value,
[key]: exists ? current.map((item) => String(item.value) === String(area.id) ? nextItem : item) : [...current, nextItem]
}
}
function isWarehouseAreaMatched(pallet, area) {
return Boolean(pallet?.warehouseId && area?.warehouseId && String(pallet.warehouseId) === String(area.warehouseId))
}
function applyScannedWarehouseArea(index, area) {
const pallet = selectedPallets.value[index]
if (!pallet || !area?.id) return
cacheWarehouseArea(area)
pallet.areaId = area.id
pallet.areaName = getWarehouseAreaLabel(area)
pallet._areaScanInput = ''
}
async function onPalletAreaScanConfirm(index) {
const key = getPalletAreaTimerKey(index)
if (palletAreaScanTimers[key]) {
clearTimeout(palletAreaScanTimers[key])
palletAreaScanTimers[key] = null
}
const pallet = selectedPallets.value[index]
if (!pallet) return
const scanCode = getWarehouseAreaScanCode(pallet._areaScanInput)
if (!scanCode) return
try {
uni.showLoading({ title: t('productInbound.loading'), mask: true })
const matched = await findWarehouseAreaByScanCode(scanCode)
uni.hideLoading()
if (!matched?.id) {
uni.showToast({ title: '\u6ca1\u6709\u8fd9\u4e2a\u5e93\u533a', icon: 'none' })
return
}
if (!pallet.warehouseId) {
uni.showToast({ title: '\u8bf7\u5148\u9009\u62e9\u4ed3\u5e93', icon: 'none' })
return
}
if (!isWarehouseAreaMatched(pallet, matched)) {
pallet._areaScanInput = ''
uni.showToast({ title: '\u5e93\u533a\u4e0d\u5c5e\u4e8e\u5f53\u524d\u4ed3\u5e93', icon: 'none' })
return
}
applyScannedWarehouseArea(index, matched)
} catch (e) {
uni.hideLoading()
uni.showToast({ title: t('productInbound.loadFailed'), icon: 'none' })
}
}
function goSelectPallet() {
if (!productId.value) {
uni.showToast({ title: t('productInbound.selectProductFirst'), icon: 'none' })
@ -617,7 +749,7 @@ function removePallet(index) {
}
async function loadWarehouses() {
try {
const res = await getWarehouseSimpleList()
const res = await getWarehouseSimpleList({ categoryType: WAREHOUSE_CATEGORY_TYPE })
const data = Array.isArray(res) ? res : (Array.isArray(res?.data) ? res.data : [])
warehouseOptions.value = data.map((w) => ({ value: w.id, label: w.name || String(w.id || '') }))
} catch (e) {}
@ -816,17 +948,21 @@ onShow(async () => {
.pallet-top { display: flex; align-items: center; justify-content: space-between; gap: 12rpx; }
.pallet-code { flex: 1; min-width: 0; font-size: 28rpx; color: #1f2937; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.remove-btn { width: 48rpx; height: 48rpx; border-radius: 24rpx; background: #fef2f2; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
.pallet-loc { display: grid; grid-template-columns: 1fr 1fr; gap: 12rpx; }
.loc-cell { min-width: 0; display: flex; flex-direction: column; gap: 6rpx; }
.pallet-field-list { display: flex; flex-direction: column; gap: 14rpx; }
.loc-cell { min-width: 0; display: flex; flex-direction: column; gap: 8rpx; }
.loc-label { font-size: 22rpx; color: #9ca3af; }
.loc-picker { min-height: 64rpx; padding: 0 16rpx; background: #f8fafc; border: 1rpx solid #e5e7eb; border-radius: 12rpx; display: flex; align-items: center; justify-content: space-between; gap: 8rpx; box-sizing: border-box; }
.loc-value { flex: 1; min-width: 0; font-size: 25rpx; color: #1f2937; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.loc-placeholder { flex: 1; font-size: 25rpx; color: #9ca3af; }
.pallet-count-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12rpx; }
.count-cell { min-width: 0; display: flex; flex-direction: column; gap: 6rpx; }
.loc-picker { min-height: 68rpx; padding: 0 18rpx; background: #f8fafc; border: 1rpx solid #e5e7eb; border-radius: 12rpx; display: flex; align-items: center; justify-content: space-between; gap: 8rpx; box-sizing: border-box; }
.pallet-field-control { width: 100%; }
.loc-value { flex: 1; min-width: 0; font-size: 26rpx; color: #1f2937; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.loc-placeholder { flex: 1; font-size: 26rpx; color: #9ca3af; }
.area-select-row { width: 100%; }
.area-select-picker { flex-shrink: 0; width: 160rpx; display: block; }
.area-select-button { width: 160rpx; }
.area-select-button.disabled { background: #94a3b8; }
.count-label { font-size: 22rpx; color: #9ca3af; }
.count-input { height: 64rpx; padding: 0 16rpx; background: #f8fafc; border: 1rpx solid #e5e7eb; border-radius: 12rpx; font-size: 26rpx; color: #1f2937; box-sizing: border-box; }
.count-value { height: 64rpx; line-height: 64rpx; padding: 0 16rpx; background: #f1f5f9; border-radius: 12rpx; font-size: 26rpx; color: #475569; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.count-input { height: 68rpx; padding: 0 18rpx; background: #f8fafc; border: 1rpx solid #e5e7eb; border-radius: 12rpx; font-size: 26rpx; color: #1f2937; box-sizing: border-box; }
.pallet-piece-row { min-height: 44rpx; display: flex; align-items: center; justify-content: space-between; gap: 16rpx; }
.pallet-piece-value { flex: 1; min-width: 0; font-size: 28rpx; font-weight: 600; color: #475569; text-align: right; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.quantity-summary { display: grid; grid-template-columns: 1fr 1fr; gap: 14rpx; }
.quantity-item { padding: 18rpx; background: #eff6ff; border-radius: 14rpx; display: flex; flex-direction: column; align-items: center; gap: 6rpx; }
.quantity-value { font-size: 34rpx; color: #1f4b79; font-weight: 700; }

Loading…
Cancel
Save