You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
besure_app/src/pages_function/pages/productInbound/productConfirm.vue

976 lines
44 KiB
Vue

<template>
<view class="page-container">
<NavBar :title="t('productInbound.createTitle')" />
<scroll-view scroll-y class="detail-scroll">
<view class="content-section">
<view class="section-card">
<view class="section-header">
<view class="section-icon">
<uni-icons type="paperplane" size="24" color="#1f7cff"></uni-icons>
</view>
<text class="section-title">{{ t('productInbound.taskInfo') }}</text>
</view>
<view class="form-field">
<text class="form-label">{{ t('productInbound.relatedTask') }}<text class="required-star">*</text></text>
<view class="segment-control">
<view :class="['segment-item', relateTask ? 'active' : '']" @click="setRelateTask(true)">{{ t('productInbound.yes') }}</view>
<view :class="['segment-item', !relateTask ? 'active' : '']" @click="setRelateTask(false)">{{ t('productInbound.no') }}</view>
</view>
</view>
<view v-if="relateTask" class="form-field">
<text class="form-label">{{ t('productInbound.taskOrder') }}<text class="required-star">*</text></text>
<view :class="['select-field', taskCode ? 'selected' : '']" @click="goSelectTask">
<view class="select-content">
<text :class="taskCode ? 'select-value' : 'select-placeholder'">{{ taskCode || t('productInbound.selectTask') }}</text>
</view>
<uni-icons type="right" size="18" color="#9ca3af"></uni-icons>
</view>
</view>
<view v-if="relateTask && taskId" class="form-field">
<text class="form-label">{{ t('productInbound.product') }}<text class="required-star">*</text></text>
<view class="task-product-search-row">
<view class="scan-input-wrap">
<input
id="product-inbound-task-product-scan-input"
v-model="taskProductScanInput"
class="task-product-scan-input"
type="text"
:placeholder="t('productInbound.selectProduct')"
confirm-type="done"
@input="onTaskProductScanInput"
@confirm="onTaskProductScanConfirm"
/>
<view class="scan-input-icon">
<uni-icons type="scan" size="20" color="#9ca3af"></uni-icons>
</view>
</view>
<view class="task-product-select-field" @click="goSelectTaskProduct">
<text class="task-product-select-text">{{ t('productInbound.selectProductTitle') }}</text>
</view>
</view>
</view>
<view v-if="!relateTask" class="form-field">
<text class="form-label">{{ t('productInbound.product') }}<text class="required-star">*</text></text>
<view class="task-product-search-row">
<view class="scan-input-wrap">
<input
id="product-inbound-product-scan-input"
v-model="productScanInput"
class="task-product-scan-input"
type="text"
:placeholder="t('productInbound.selectProduct')"
confirm-type="done"
@input="onProductScanInput"
@confirm="onProductScanConfirm"
/>
<view class="scan-input-icon">
<uni-icons type="scan" size="20" color="#9ca3af"></uni-icons>
</view>
</view>
<view class="task-product-select-field" @click="goSelectProduct">
<text class="task-product-select-text">{{ t('productInbound.selectProductTitle') }}</text>
</view>
</view>
</view>
<view v-if="productId" class="info-panel">
<view class="info-grid">
<view v-if="relateTask" class="info-item">
<text class="info-label">{{ t('productInbound.workOrderNo') }}</text>
<text class="info-value">{{ textValue(taskCode) }}</text>
</view>
<view class="info-item">
<text class="info-label">{{ t('productInbound.product') }}</text>
<text class="info-value">{{ textValue(productName) }}</text>
</view>
<view v-if="!relateTask" class="info-item">
<text class="info-label">{{ t('productInbound.code') }}</text>
<text class="info-value">{{ textValue(productBarCode) }}</text>
</view>
<view class="info-item">
<text class="info-label">{{ t('productInbound.packagingScheme') }}</text>
<text class="info-value">{{ textValue(packagingSchemeName) }}</text>
</view>
<view class="info-item">
<text class="info-label">{{ t('productInbound.palletPackageQuantity') }}</text>
<text class="info-value">{{ t('productInbound.packageUnit', { count: textValue(palletPackageQuantity) }) }}</text>
</view>
<view class="info-item">
<text class="info-label">{{ t('productInbound.packageQuantity') }}</text>
<text class="info-value">{{ t('productInbound.pieceUnit', { count: textValue(packageQuantity) }) }}</text>
</view>
</view>
</view>
</view>
<view class="section-card">
<view class="section-header">
<view class="section-icon">
<uni-icons type="list" size="24" color="#1f7cff"></uni-icons>
</view>
<text class="section-title">{{ t('productInbound.pallet') }}</text>
</view>
<view class="form-field">
<view class="form-label-row">
<text class="form-label">{{ t('productInbound.palletCode') }}<text class="required-star">*</text></text>
<text v-if="selectedPallets.length" class="label-tip">{{ t('productInbound.selectedPalletCount', { count: selectedPallets.length }) }}</text>
</view>
<view class="task-product-search-row">
<view class="scan-input-wrap">
<input
id="product-inbound-pallet-scan-input"
v-model="palletScanInput"
class="task-product-scan-input"
type="text"
:placeholder="palletCode || t('productInbound.selectPallet')"
confirm-type="done"
@input="onPalletScanInput"
@confirm="onPalletScanConfirm"
/>
<view class="scan-input-icon">
<uni-icons type="scan" size="20" color="#9ca3af"></uni-icons>
</view>
</view>
<view class="task-product-select-field" @click="goSelectPallet">
<text class="task-product-select-text">{{ t('productInbound.selectPalletTitle') }}</text>
</view>
</view>
</view>
<view v-if="selectedPallets.length" class="pallet-list">
<view v-for="(pallet, index) in selectedPallets" :key="pallet.id || index" class="pallet-card">
<view class="pallet-main">
<view class="pallet-top">
<text class="pallet-code">{{ textValue(getPalletCode(pallet)) }}</text>
<view class="remove-btn" @click="removePallet(index)">
<uni-icons type="closeempty" size="18" color="#ef4444"></uni-icons>
</view>
</view>
<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 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.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>
<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 class="loc-cell">
<text class="count-label">{{ t('productInbound.packageCount') }}</text>
<input class="count-input pallet-field-control" type="number" :value="pallet.packageCount" @input="onPalletCountInput($event, index)" placeholder="0" />
</view>
<view class="pallet-piece-row">
<text class="count-label">{{ t('productInbound.pieceCount') }}</text>
<text class="pallet-piece-value">{{ getPalletItemCount(pallet) }}</text>
</view>
</view>
</view>
</view>
<view class="quantity-summary">
<view class="quantity-item">
<text class="quantity-value">{{ inputCount }}</text>
<text class="quantity-label">{{ t('productInbound.packageCount') }}</text>
</view>
<view class="quantity-item">
<text class="quantity-value">{{ count }}</text>
<text class="quantity-label">{{ t('productInbound.pieceCount') }}</text>
</view>
</view>
</view>
<view v-else class="empty-card" @click="goSelectPallet">{{ t('productInbound.selectPalletFirst') }}</view>
</view>
</view>
</scroll-view>
<view class="action-bar">
<view class="action-btn back-btn" @click="handleCancel">{{ t('productInbound.cancel') }}</view>
<view class="action-btn submit-btn" @click="handleConfirm">{{ t('productInbound.confirm') }}</view>
</view>
</view>
</template>
<script setup>
import { computed, ref } from 'vue'
import { onLoad, onShow } from '@dcloudio/uni-app'
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 { getWarehouseArea, getWarehouseAreaSimpleList, getWarehouseSimpleList } from '@/api/mes/moldget'
const { t } = useI18n()
const product = ref({})
const relateTask = ref(true)
const taskId = ref(null)
const taskCode = ref('')
const selectedTaskData = ref(null)
const selectedTaskProduct = ref(null)
const selectedPallets = ref([])
const editingIndex = ref(null)
const taskProductScanInput = ref('')
const productScanInput = ref('')
const palletScanInput = ref('')
const warehouseOptions = ref([])
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)
const productBarCode = computed(() => selectedTaskProduct.value?.productCode || product.value.barCode || product.value.code || '')
const currentScheme = computed(() => selectedTaskProduct.value ? getDefaultScheme(selectedTaskProduct.value) : getDefaultScheme(product.value))
const packagingSchemeName = computed(() => currentScheme.value?.packagingSchemeName || '')
const packageQuantity = computed(() => Number(currentScheme.value?.packageQuantity) || 0)
const palletPackageQuantity = computed(() => Number(currentScheme.value?.palletPackageQuantity) || 0)
const palletTotalQuantity = computed(() => Number(currentScheme.value?.palletTotalQuantity) || packageQuantity.value * palletPackageQuantity.value)
const inputCount = computed(() => {
if (!selectedPallets.value.length) return 0
return selectedPallets.value.reduce((sum, pallet) => sum + (Number(pallet.packageCount) || 0), 0)
})
const count = computed(() => {
return selectedPallets.value.reduce((sum, pallet) => {
const packageCount = Number(pallet.packageCount) || 0
return sum + packageCount * (packageQuantity.value || 1)
}, 0)
})
const palletCode = computed(() => selectedPallets.value.map((item) => getPalletCode(item)).filter(Boolean).join(', '))
function cloneData(value) {
if (value === null || value === undefined) return value
return JSON.parse(JSON.stringify(value))
}
function buildSchemeFromItem(item) {
return {
id: item?.packagingSchemeRelationId,
packagingSchemeId: item?.packagingSchemeId,
packagingSchemeName: item?.packagingSchemeName,
packageQuantity: item?.packageQuantity,
palletPackageQuantity: item?.palletPackageQuantity,
palletTotalQuantity: item?.palletTotalQuantity,
defaultStatus: 1
}
}
function normalizeEditPallets(item) {
return (Array.isArray(item?.pallets) ? item.pallets : []).map((pallet) => ({
...pallet,
id: pallet.id || pallet.palletId,
code: pallet.code || pallet.palletCode,
palletCode: pallet.palletCode || pallet.code,
packageCount: Number(pallet.packageCount) || 0,
warehouseId: pallet.warehouseId,
areaId: pallet.areaId
}))
}
async function hydrateEditItem(item) {
if (!item) return
const scheme = buildSchemeFromItem(item)
relateTask.value = item.relateTask === true || item.relateTask === 1 || item.relateTask === '1'
taskId.value = relateTask.value ? item.taskId : null
taskCode.value = relateTask.value ? (item.taskCode || '') : ''
selectedTaskData.value = item._selectedTaskData ? cloneData(item._selectedTaskData) : null
product.value = item._product
? cloneData(item._product)
: {
id: item.productId,
name: item.productName,
barCode: item.productBarCode,
code: item.productBarCode,
unitName: item.productUnitName,
images: item.images,
packagingSchemes: [scheme]
}
if (relateTask.value) {
selectedTaskProduct.value = item._selectedTaskProduct
? cloneData(item._selectedTaskProduct)
: {
productId: item.productId,
productName: item.productName,
productCode: item.productBarCode,
taskDetailIds: item.taskDetailIds,
packagingSchemes: [scheme]
}
if (taskId.value) {
try {
const res = await getTaskDefaultPackagingSchemes(taskId.value)
selectedTaskData.value = res?.data || res || selectedTaskData.value || {}
taskProductList.value = Array.isArray(selectedTaskData.value?.products) ? selectedTaskData.value.products : []
} catch (e) {
taskProductList.value = Array.isArray(selectedTaskData.value?.products) ? selectedTaskData.value.products : []
}
}
} else {
selectedTaskProduct.value = null
selectedTaskData.value = null
taskProductList.value = []
}
selectedPallets.value = normalizeEditPallets(item)
await loadAreasForPallets(selectedPallets.value)
}
function textValue(v) {
if (v === 0) return '0'
if (v == null) return '-'
const s = String(v).trim()
return s || '-'
}
function getDefaultScheme(item) {
const schemes = Array.isArray(item?.packagingSchemes) ? item.packagingSchemes : []
return schemes.find((scheme) => Number(scheme?.defaultStatus) === 1) || schemes[0] || {}
}
function getPalletCode(pallet) {
return pallet?.code || pallet?.palletCode || ''
}
function getWarehouseName(id) {
return warehouseOptions.value.find((item) => String(item.value) === String(id))?.label || ''
}
function getAreaName(warehouseId, areaId) {
const areas = warehouseAreaMap.value[String(warehouseId)] || []
return areas.find((item) => String(item.value) === String(areaId))?.label || ''
}
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
}
function getAreaIndex(warehouseId, areaId) {
const areas = getAreaOptions(warehouseId)
const i = areas.findIndex((item) => String(item.value) === String(areaId))
return i >= 0 ? i : 0
}
function getPalletItemCount(pallet) {
const c = Number(pallet?.packageCount) || 0
return c * (packageQuantity.value || 1)
}
async function onPalletWarehouseChange(e, index) {
const opt = warehouseOptions.value[Number(e.detail.value)]
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)
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)
selectedPallets.value[index].packageCount = Number.isFinite(v) ? v : 0
}
function setRelateTask(value) {
if (relateTask.value === value) return
relateTask.value = value
product.value = {}
selectedTaskProduct.value = null
taskProductScanInput.value = ''
productScanInput.value = ''
palletScanInput.value = ''
taskId.value = null
taskCode.value = ''
selectedTaskData.value = null
selectedPallets.value = []
}
function goSelectTask() {
const suffix = taskId.value ? `?selectedId=${taskId.value}` : ''
uni.navigateTo({ url: `/pages_function/pages/productInbound/taskSelect${suffix}` })
}
function goSelectProduct() {
const suffix = product.value?.id ? `?selectedId=${product.value.id}` : ''
uni.navigateTo({ url: `/pages_function/pages/productInbound/productSelect${suffix}` })
}
function goSelectTaskProduct() {
if (!taskId.value) {
uni.showToast({ title: t('productInbound.selectTaskFirst'), icon: 'none' })
return
}
const suffix = selectedTaskProduct.value?.productId ? `?selectedId=${selectedTaskProduct.value.productId}` : ''
uni.navigateTo({ url: `/pages_function/pages/productInbound/productSelect${suffix}` })
}
function normalizeScanValue(value) {
return String(value || '').trim().toLowerCase()
}
function getProductMaterialScanCode(value) {
const text = String(value || '').trim()
const match = text.match(/^PRODUCTMATERIAL[-:]?\s*(.+)$/i)
return match ? match[1].trim() : ''
}
function getPalletScanCode(value) {
const text = String(value || '').trim()
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
}
function getProductNameValue(item) {
return item?.name || item?.productName || ''
}
function getProductCodeValue(item) {
return item?.barCode || item?.code || item?.productCode || item?.productBarCode || ''
}
function getTaskProductScanValues(item) {
return [
item?.productId,
item?.productCode,
item?.productBarCode,
item?.barCode,
item?.code,
item?.productName,
item?.name
].map(normalizeScanValue).filter(Boolean)
}
async function findProductByScanCode(scanCode) {
const id = String(scanCode || '').trim()
if (!/^\d+$/.test(id)) return null
const res = await getProduct(id)
const detail = res?.data || res
if (!detail?.id) return null
if (detail.categoryType && Number(detail.categoryType) !== PRODUCT_CATEGORY_TYPE) return null
return detail
}
function getTaskProductByProduct(productItem) {
const productValues = [getProductIdValue(productItem), getProductCodeValue(productItem), getProductNameValue(productItem)].map(normalizeScanValue).filter(Boolean)
return taskProductList.value.find((item) => {
const values = getTaskProductScanValues(item)
return productValues.some((value) => values.includes(value))
})
}
function buildTaskProductSelection(productItem) {
const taskProduct = getTaskProductByProduct(productItem)
return {
...productItem,
...(taskProduct || {}),
productId: getProductIdValue(productItem),
productName: getProductNameValue(productItem),
productCode: getProductCodeValue(productItem),
packagingSchemes: taskProduct?.packagingSchemes || productItem?.packagingSchemes || []
}
}
function scheduleTaskProductScanSearch(value) {
if (!getProductMaterialScanCode(value)) return
if (taskProductScanTimer) clearTimeout(taskProductScanTimer)
taskProductScanTimer = setTimeout(() => {
taskProductScanTimer = null
onTaskProductScanConfirm()
}, SCAN_AUTO_SEARCH_DELAY)
}
function scheduleProductScanSearch(value) {
if (!getProductMaterialScanCode(value)) return
if (productScanTimer) clearTimeout(productScanTimer)
productScanTimer = setTimeout(() => {
productScanTimer = null
onProductScanConfirm()
}, SCAN_AUTO_SEARCH_DELAY)
}
function onTaskProductScanInput(e) {
scheduleTaskProductScanSearch(e?.detail?.value ?? taskProductScanInput.value)
}
function onProductScanInput(e) {
scheduleProductScanSearch(e?.detail?.value ?? productScanInput.value)
}
async function onTaskProductScanConfirm() {
if (taskProductScanTimer) {
clearTimeout(taskProductScanTimer)
taskProductScanTimer = null
}
const scanCode = getProductMaterialScanCode(taskProductScanInput.value)
if (!scanCode) return
if (!taskId.value) {
uni.showToast({ title: t('productInbound.selectTaskFirst'), icon: 'none' })
return
}
try {
uni.showLoading({ title: t('productInbound.loading'), mask: true })
const matched = await findProductByScanCode(scanCode)
uni.hideLoading()
if (!matched?.id) {
uni.showToast({ title: '\u6ca1\u6709\u8fd9\u4e2a\u4ea7\u54c1', icon: 'none' })
return
}
selectedTaskProduct.value = buildTaskProductSelection(matched)
selectedPallets.value = []
palletScanInput.value = ''
taskProductScanInput.value = ''
} catch (e) {
uni.hideLoading()
uni.showToast({ title: t('productInbound.loadFailed'), icon: 'none' })
}
}
async function onProductScanConfirm() {
if (productScanTimer) {
clearTimeout(productScanTimer)
productScanTimer = null
}
const scanCode = getProductMaterialScanCode(productScanInput.value)
if (!scanCode) return
try {
uni.showLoading({ title: t('productInbound.loading'), mask: true })
const matched = await findProductByScanCode(scanCode)
uni.hideLoading()
if (!matched?.id) {
uni.showToast({ title: '\u6ca1\u6709\u8fd9\u4e2a\u4ea7\u54c1', icon: 'none' })
return
}
product.value = matched
selectedTaskProduct.value = null
selectedPallets.value = []
palletScanInput.value = ''
productScanInput.value = ''
} catch (e) {
uni.hideLoading()
uni.showToast({ title: t('productInbound.loadFailed'), icon: 'none' })
}
}
function schedulePalletScanSearch(value) {
if (!getPalletScanCode(value)) return
if (palletScanTimer) clearTimeout(palletScanTimer)
palletScanTimer = setTimeout(() => {
palletScanTimer = null
onPalletScanConfirm()
}, SCAN_AUTO_SEARCH_DELAY)
}
function onPalletScanInput(e) {
schedulePalletScanSearch(e?.detail?.value ?? palletScanInput.value)
}
function addScannedPallet(pallet) {
if (!pallet?.id) return
const exists = selectedPallets.value.some((item) => String(item.id) === String(pallet.id))
if (exists) return
selectedPallets.value.push({
...pallet,
packageCount: pallet.productCount || pallet.packageCount || palletPackageQuantity.value || 1
})
}
async function findPalletByScanCode(scanCode) {
const id = String(scanCode || '').trim()
if (!/^\d+$/.test(id)) return null
const res = await getPallet(id)
const detail = res?.data || res
if (!detail?.id) return null
if (detail.productId && productId.value && String(detail.productId) !== String(productId.value)) return null
return detail
}
async function onPalletScanConfirm() {
if (palletScanTimer) {
clearTimeout(palletScanTimer)
palletScanTimer = null
}
const scanCode = getPalletScanCode(palletScanInput.value)
if (!scanCode) return
if (!productId.value) {
uni.showToast({ title: t('productInbound.selectProductFirst'), icon: 'none' })
return
}
try {
uni.showLoading({ title: t('productInbound.loading'), mask: true })
const matched = await findPalletByScanCode(scanCode)
uni.hideLoading()
if (!matched?.id) {
uni.showToast({ title: '\u6ca1\u6709\u8fd9\u4e2a\u6258\u76d8', icon: 'none' })
return
}
addScannedPallet(matched)
await loadAreasForPallets([matched])
palletScanInput.value = ''
} catch (e) {
uni.hideLoading()
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' })
return
}
getApp().globalData._productInboundSelectedPallets = [...selectedPallets.value]
const query = `productId=${productId.value}&packageCount=${palletPackageQuantity.value || 1}`
uni.navigateTo({ url: `/pages_function/pages/productInbound/palletSelect?${query}` })
}
function removePallet(index) {
selectedPallets.value.splice(index, 1)
}
async function loadWarehouses() {
try {
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) {}
}
async function loadAreasForWarehouse(warehouseId) {
if (!warehouseId || warehouseAreaMap.value[String(warehouseId)]) return
try {
const res = await getWarehouseAreaSimpleList(warehouseId)
const data = Array.isArray(res) ? res : (Array.isArray(res?.data) ? res.data : [])
warehouseAreaMap.value = {
...warehouseAreaMap.value,
[String(warehouseId)]: data.map((a) => ({ value: a.id, label: a.name || a.areaName || String(a.id || '') }))
}
} catch (e) {
warehouseAreaMap.value = { ...warehouseAreaMap.value, [String(warehouseId)]: [] }
}
}
async function loadAreasForPallets(pallets) {
const warehouseIds = Array.from(new Set((pallets || []).map((p) => p.warehouseId).filter(Boolean)))
await Promise.all(warehouseIds.map((id) => loadAreasForWarehouse(id)))
}
function handleCancel() {
const gd = getApp().globalData
gd._productInboundEditIndex = null
gd._productInboundEditItem = null
uni.navigateBack()
}
function handleConfirm() {
if (relateTask.value && !taskId.value) {
uni.showToast({ title: t('productInbound.selectTask'), icon: 'none' })
return
}
if (relateTask.value && !selectedTaskProduct.value) {
uni.showToast({ title: t('productInbound.selectTaskProduct'), icon: 'none' })
return
}
if (!productId.value) {
uni.showToast({ title: t('productInbound.selectProduct'), icon: 'none' })
return
}
if (!selectedPallets.value.length) {
uni.showToast({ title: t('productInbound.selectPallet'), icon: 'none' })
return
}
const invalid = selectedPallets.value.find((p) => !p.warehouseId || !p.areaId || !Number(p.packageCount))
if (invalid) {
uni.showToast({ title: t('productInbound.completePalletInfo'), icon: 'none' })
return
}
const firstPallet = selectedPallets.value[0]
const item = {
productId: productId.value,
productName: productName.value,
productBarCode: productBarCode.value,
productUnitName: product.value.unitName || product.value.minStockUnitName || '个',
inputUnitType: '个',
inputCount: inputCount.value,
count: count.value,
relateTask: relateTask.value,
taskId: relateTask.value ? taskId.value : null,
taskCode: taskCode.value,
taskDetailIds: selectedTaskProduct.value?.taskDetailIds,
packagingSchemeRelationId: currentScheme.value?.id,
packagingSchemeId: currentScheme.value?.packagingSchemeId,
packagingSchemeName: packagingSchemeName.value,
packageQuantity: packageQuantity.value,
palletPackageQuantity: palletPackageQuantity.value,
palletTotalQuantity: palletTotalQuantity.value,
warehouseId: firstPallet?.warehouseId || null,
warehouseName: getWarehouseName(firstPallet?.warehouseId),
areaId: firstPallet?.areaId || null,
areaName: getAreaName(firstPallet?.warehouseId, firstPallet?.areaId),
palletCode: palletCode.value,
pallets: selectedPallets.value.map((pallet) => ({
palletId: pallet.id,
palletCode: getPalletCode(pallet),
packageCount: Number(pallet.packageCount) || 0,
warehouseId: pallet.warehouseId,
areaId: pallet.areaId,
count: (Number(pallet.packageCount) || 0) * (packageQuantity.value || 1)
})),
images: product.value.images,
_product: { ...product.value },
_selectedTaskProduct: selectedTaskProduct.value ? { ...selectedTaskProduct.value } : null,
_selectedTaskData: selectedTaskData.value ? { ...selectedTaskData.value } : null
}
const gd = getApp().globalData
if (!gd._productInboundItems) gd._productInboundItems = []
const index = Number(editingIndex.value)
if (Number.isInteger(index) && index >= 0 && index < gd._productInboundItems.length) {
gd._productInboundItems.splice(index, 1, item)
} else {
gd._productInboundItems.push(item)
}
gd._productInboundEditIndex = null
gd._productInboundEditItem = null
uni.showToast({ title: t('productInbound.productAdded'), icon: 'success', duration: 1000 })
setTimeout(() => uni.navigateBack(), 700)
}
onLoad(async () => {
const gd = getApp().globalData
const index = gd?._productInboundEditIndex
const item = gd?._productInboundEditItem
if (item && index !== null && index !== undefined) {
editingIndex.value = Number(index)
gd._productInboundEditIndex = null
gd._productInboundEditItem = null
await hydrateEditItem(item)
}
})
onShow(async () => {
const gd = getApp().globalData
const taskResult = gd?._productInboundTaskSelectResult
if (taskResult) {
taskId.value = taskResult.id
taskCode.value = taskResult.code || taskResult.taskName || ''
selectedTaskProduct.value = null
taskProductScanInput.value = ''
selectedPallets.value = []
palletScanInput.value = ''
gd._productInboundTaskSelectResult = null
try {
uni.showLoading({ title: t('productInbound.loading'), mask: true })
const res = await getTaskDefaultPackagingSchemes(taskResult.id)
uni.hideLoading()
selectedTaskData.value = res?.data || res || {}
taskProductList.value = Array.isArray(selectedTaskData.value?.products) ? selectedTaskData.value.products : []
} catch (e) {
uni.hideLoading()
uni.showToast({ title: t('productInbound.taskProductLoadFailed'), icon: 'none' })
}
}
const productResult = gd?._productInboundProductSelectResult
if (productResult) {
product.value = productResult
if (relateTask.value) {
selectedTaskProduct.value = buildTaskProductSelection(productResult)
taskProductScanInput.value = ''
} else {
selectedTaskProduct.value = null
productScanInput.value = ''
}
selectedPallets.value = []
palletScanInput.value = ''
gd._productInboundProductSelectResult = null
}
const palletResult = gd?._productInboundPalletSelectResult
if (Array.isArray(palletResult)) {
selectedPallets.value = palletResult
await loadAreasForPallets(selectedPallets.value)
gd._productInboundPalletSelectResult = null
}
await loadWarehouses()
})
</script>
<style lang="scss" scoped>
.page-container { min-height: 100vh; background: #f5f7fb; }
.detail-scroll { height: calc(100vh - 172rpx); }
.content-section { padding: 20rpx 24rpx 28rpx; }
.section-card { background: #ffffff; border-radius: 20rpx; padding: 24rpx; margin-bottom: 20rpx; border: 1rpx solid #eef2f7; box-shadow: 0 6rpx 18rpx rgba(15, 23, 42, 0.04); }
.section-header { display: flex; align-items: center; gap: 12rpx; margin-bottom: 22rpx; padding-bottom: 18rpx; border-bottom: 1rpx solid #f1f5f9; }
.section-icon { width: 40rpx; height: 40rpx; border-radius: 10rpx; background: #eff6ff; display: flex; align-items: center; justify-content: center; }
.section-title { font-size: 32rpx; font-weight: 600; color: #1f2937; }
.form-field { display: flex; flex-direction: column; gap: 12rpx; }
.form-field + .form-field { margin-top: 24rpx; }
.form-label-row { display: flex; align-items: center; justify-content: space-between; gap: 16rpx; }
.form-label { font-size: 26rpx; color: #4b5563; font-weight: 500; }
.label-tip { flex-shrink: 0; font-size: 24rpx; color: #6b7280; }
.required-star { color: #ef4444; font-size: 28rpx; margin-left: 4rpx; }
.segment-control { display: grid; grid-template-columns: 1fr 1fr; gap: 12rpx; padding: 6rpx; background: #f3f4f6; border-radius: 16rpx; }
.segment-item { height: 72rpx; border-radius: 12rpx; display: flex; align-items: center; justify-content: center; color: #6b7280; font-size: 28rpx; font-weight: 600; }
.segment-item.active { background: #ffffff; color: #1f7cff; box-shadow: 0 4rpx 12rpx rgba(15, 23, 42, 0.08); }
.select-field { min-height: 70rpx; padding: 16rpx 22rpx; background: #f8fafc; border: 1rpx solid #e5e7eb; border-radius: 14rpx; box-sizing: border-box; display: flex; align-items: center; justify-content: space-between; gap: 12rpx; }
.select-field.selected { background: #f9fbff; border-color: #bfdbfe; box-shadow: 0 4rpx 12rpx rgba(31, 124, 255, 0.08); }
.select-content { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 6rpx; }
.select-value { font-size: 28rpx; font-weight: 600; color: #1f2937; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.select-placeholder { font-size: 28rpx; color: #9ca3af; }
.select-subtext { font-size: 24rpx; color: #6b7280; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.task-product-search-row { display: flex; align-items: center; gap: 16rpx; }
.scan-input-wrap { position: relative; flex: 1; min-width: 0; }
.task-product-scan-input { width: 100%; height: 70rpx; padding: 0 64rpx 0 22rpx; background: #f8fafc; border: 1rpx solid #e5e7eb; border-radius: 14rpx; box-sizing: border-box; font-size: 28rpx; color: #1f2937; }
.scan-input-icon { position: absolute; right: 20rpx; top: 0; bottom: 0; display: flex; align-items: center; justify-content: center; pointer-events: none; }
.task-product-select-field { flex-shrink: 0; width: 160rpx; min-height: 70rpx; padding: 0 18rpx; background: #1f4b79; border-radius: 14rpx; box-sizing: border-box; display: flex; align-items: center; justify-content: center; }
.task-product-select-text { flex: 1; min-width: 0; font-size: 26rpx; font-weight: 600; color: #ffffff; text-align: center; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.info-panel { margin-top: 22rpx; padding: 20rpx; background: #f8fafc; border: 1rpx solid #e8eef6; border-radius: 16rpx; }
.info-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0; background: #ffffff; border: 1rpx solid #eef2f7; border-radius: 14rpx; overflow: hidden; }
.info-item { min-width: 0; display: flex; flex-direction: column; gap: 8rpx; padding: 18rpx 20rpx; border-right: 1rpx solid #f1f5f9; border-bottom: 1rpx solid #f1f5f9; }
.info-item:nth-child(2n) { border-right: 0; }
.info-label { font-size: 23rpx; color: #8a94a6; }
.info-value { font-size: 27rpx; color: #334155; line-height: 1.35; word-break: break-all; }
.pallet-list { margin-top: 20rpx; display: flex; flex-direction: column; gap: 14rpx; }
.pallet-card { padding: 18rpx 20rpx; border: 1rpx solid #eef2f7; border-radius: 16rpx; background: #ffffff; }
.pallet-main { display: flex; flex-direction: column; gap: 14rpx; }
.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-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: 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: 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; }
.quantity-label { font-size: 23rpx; color: #64748b; }
.empty-card { height: 96rpx; border-radius: 14rpx; background: #f8fafc; color: #94a3b8; font-size: 27rpx; display: flex; align-items: center; justify-content: center;margin-top: 20rpx; }
.action-bar { position: fixed; left: 0; right: 0; bottom: 0; display: flex; gap: 18rpx; padding: 18rpx 24rpx calc(18rpx + env(safe-area-inset-bottom)); background: #ffffff; box-shadow: 0 -8rpx 24rpx rgba(15, 23, 42, 0.06); z-index: 99; }
.action-btn { flex: 1; height: 84rpx; border-radius: 16rpx; display: flex; align-items: center; justify-content: center; font-size: 30rpx; font-weight: 600; }
.back-btn { background: #eef2f7; color: #475569; }
.submit-btn { background: #1f4b79; color: #ffffff; }
</style>