|
|
|
|
@ -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; }
|
|
|
|
|
|