feat:产品入库-产品选择框支持扫码录入

master
黄伟杰 10 hours ago
parent cc2aef824d
commit 7890b0d429

@ -1,2 +1,3 @@
VITE_APP_BASE_URL=http://192.168.43.233:48081
VITE_APP_BASE_URL=http://192.168.5.37:48081
# VITE_APP_BASE_URL=http://192.168.43.233:48081
# VITE_APP_BASE_URL=http://47.106.185.127:8089

@ -465,13 +465,6 @@
"navigationStyle": "custom"
}
},
{
"path": "productInbound/taskProductSelect",
"style": {
"navigationBarTitleText": "选择任务产品",
"navigationStyle": "custom"
}
},
{
"path": "productOutbound/index",
"style": {

@ -31,15 +31,16 @@
</view>
<view v-if="relateTask && taskId" class="form-field">
<text class="form-label">{{ t('productInbound.taskProduct') }}<text class="required-star">*</text></text>
<text class="form-label">{{ t('productInbound.product') }}<text class="required-star">*</text></text>
<view class="task-product-search-row">
<input
id="product-inbound-task-product-scan-input"
v-model="taskProductScanInput"
class="task-product-scan-input"
type="text"
:placeholder="t('productInbound.searchTaskProductPlaceholder')"
:placeholder="t('productInbound.selectProduct')"
confirm-type="done"
@input="onTaskProductScanInput"
@confirm="onTaskProductScanConfirm"
/>
<view class="task-product-select-field" @click="goSelectTaskProduct">
@ -56,8 +57,9 @@
v-model="productScanInput"
class="task-product-scan-input"
type="text"
:placeholder="t('productInbound.searchProductPlaceholder')"
:placeholder="t('productInbound.selectProduct')"
confirm-type="done"
@input="onProductScanInput"
@confirm="onProductScanConfirm"
/>
<view class="task-product-select-field" @click="goSelectProduct">
@ -206,6 +208,11 @@ const warehouseOptions = ref([])
const warehouseAreaMap = ref({})
const taskProductList = ref([])
const PRODUCT_CATEGORY_TYPE = 1
const PRODUCT_PAGE_SIZE = 10
const SCAN_AUTO_SEARCH_DELAY = 300
let taskProductScanTimer = null
let productScanTimer = null
const productName = computed(() => selectedTaskProduct.value?.productName || product.value.name || '')
const productId = computed(() => selectedTaskProduct.value?.productId || product.value.id)
@ -375,27 +382,25 @@ function goSelectTaskProduct() {
uni.showToast({ title: t('productInbound.selectTaskFirst'), icon: 'none' })
return
}
if (!taskProductList.value.length) {
uni.showToast({ title: t('productInbound.emptyTaskProducts'), icon: 'none' })
return
}
getApp().globalData._productInboundTaskProductList = [...taskProductList.value]
const suffix = selectedTaskProduct.value?.productId ? `?selectedId=${selectedTaskProduct.value.productId}` : ''
uni.navigateTo({ url: `/pages_function/pages/productInbound/taskProductSelect${suffix}` })
uni.navigateTo({ url: `/pages_function/pages/productInbound/productSelect${suffix}` })
}
function normalizeScanValue(value) {
return String(value || '').trim().toLowerCase()
}
function getProductScanKeywords(value) {
const rawText = String(value || '').trim()
const raw = normalizeScanValue(rawText)
if (!raw) return []
const candidates = [raw, raw.replace(/^product[-:]/i, ''), raw.replace(/^task_product[-:]/i, '')]
try {
const parsed = JSON.parse(rawText)
candidates.push(parsed?.id, parsed?.productId, parsed?.code, parsed?.productCode, parsed?.barCode)
} catch (e) {}
return [...new Set(candidates.map(normalizeScanValue).filter(Boolean))]
function getProductMaterialScanCode(value) {
const text = String(value || '').trim()
const match = text.match(/^PRODUCTMATERIAL[-:]?\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 [
@ -408,73 +413,130 @@ function getTaskProductScanValues(item) {
item?.name
].map(normalizeScanValue).filter(Boolean)
}
function onTaskProductScanConfirm() {
const keywords = getProductScanKeywords(taskProductScanInput.value)
if (!keywords.length) return
if (!taskId.value) {
uni.showToast({ title: t('productInbound.selectTaskFirst'), icon: 'none' })
return
}
if (!taskProductList.value.length) {
uni.showToast({ title: t('productInbound.emptyTaskProducts'), icon: 'none' })
return
}
const matched = taskProductList.value.find((item) => {
const values = getTaskProductScanValues(item)
return keywords.some((keyword) => values.includes(keyword))
})
if (!matched) {
uni.showToast({ title: t('productInbound.emptyTaskProducts'), icon: 'none' })
return
}
selectedTaskProduct.value = matched
selectedPallets.value = []
taskProductScanInput.value = ''
}
function normalizeProductPageList(res) {
const root = res && res.data !== undefined ? res.data : res
return Array.isArray(root) ? root : (root?.list || root?.rows || root?.records || [])
}
async function findProductByScanCode(value) {
const keywords = getProductScanKeywords(value)
const primary = keywords[0] || ''
if (!primary) return null
const productId = keywords.find((keyword) => /^\d+$/.test(keyword))
if (productId) {
function pickMatchedProduct(list, keyword) {
const normalized = normalizeScanValue(keyword)
return list.find((item) => {
const values = [getProductIdValue(item), getProductCodeValue(item), getProductNameValue(item)].map(normalizeScanValue).filter(Boolean)
return values.includes(normalized)
}) || list[0]
}
async function getProductDetailOrSelf(item) {
const id = getProductIdValue(item)
if (!id) return item
try {
const res = await getProduct(id)
return { ...item, ...(res?.data || res || {}) }
} catch (e) {
return item
}
}
async function findProductByScanCode(scanCode) {
const keyword = String(scanCode || '').trim()
if (!keyword) return null
const codeRes = await getProductPage({ pageNo: 1, pageSize: PRODUCT_PAGE_SIZE, categoryType: PRODUCT_CATEGORY_TYPE, barCode: keyword })
const codeList = normalizeProductPageList(codeRes)
let matched = pickMatchedProduct(codeList, keyword)
if (!matched) {
const nameRes = await getProductPage({ pageNo: 1, pageSize: PRODUCT_PAGE_SIZE, categoryType: PRODUCT_CATEGORY_TYPE, name: keyword })
const nameList = normalizeProductPageList(nameRes)
matched = pickMatchedProduct(nameList, keyword)
}
if (!matched && /^\d+$/.test(keyword)) {
try {
const res = await getProduct(productId)
const res = await getProduct(keyword)
const detail = res?.data || res
if (detail?.id) return detail
if (detail?.id && (!detail.categoryType || Number(detail.categoryType) === PRODUCT_CATEGORY_TYPE)) matched = detail
} catch (e) {}
}
for (let pageNo = 1; pageNo <= 5; pageNo += 1) {
const res = await getProductPage({ pageNo, pageSize: 100, categoryType: 1 })
const list = normalizeProductPageList(res)
const matched = list.find((item) => {
const values = [item?.id, item?.barCode, item?.code, item?.name].map(normalizeScanValue).filter(Boolean)
return keywords.some((keyword) => values.includes(keyword))
})
if (matched) {
try {
const detailRes = await getProduct(matched.id)
return { ...matched, ...(detailRes?.data || detailRes || {}) }
} catch (e) {
return matched
}
return matched ? getProductDetailOrSelf(matched) : null
}
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
}
if (list.length < 100) break
selectedTaskProduct.value = buildTaskProductSelection(matched)
selectedPallets.value = []
taskProductScanInput.value = ''
} catch (e) {
uni.hideLoading()
uni.showToast({ title: t('productInbound.loadFailed'), icon: 'none' })
}
return null
}
async function onProductScanConfirm() {
const keywords = getProductScanKeywords(productScanInput.value)
if (!keywords.length) return
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(productScanInput.value)
const matched = await findProductByScanCode(scanCode)
uni.hideLoading()
if (!matched?.id) {
uni.showToast({ title: t('productInbound.emptyProduct'), icon: 'none' })
uni.showToast({ title: '\u6ca1\u6709\u8fd9\u4e2a\u4ea7\u54c1', icon: 'none' })
return
}
product.value = matched
@ -633,17 +695,16 @@ onShow(async () => {
uni.showToast({ title: t('productInbound.taskProductLoadFailed'), icon: 'none' })
}
}
const taskProductResult = gd?._productInboundTaskProductSelectResult
if (taskProductResult) {
selectedTaskProduct.value = taskProductResult
taskProductScanInput.value = ''
selectedPallets.value = []
gd._productInboundTaskProductSelectResult = null
}
const productResult = gd?._productInboundProductSelectResult
if (productResult) {
product.value = productResult
productScanInput.value = ''
if (relateTask.value) {
selectedTaskProduct.value = buildTaskProductSelection(productResult)
taskProductScanInput.value = ''
} else {
selectedTaskProduct.value = null
productScanInput.value = ''
}
selectedPallets.value = []
gd._productInboundProductSelectResult = null
}

@ -3,35 +3,46 @@
<NavBar :title="t('productInbound.selectProductTitle')" />
<view class="search-bar">
<view class="search-input-wrap">
<uni-icons type="search" size="18" color="#9ca3af"></uni-icons>
<input
v-model="searchText"
class="search-input"
:placeholder="t('productInbound.searchProductPlaceholder')"
placeholder-class="search-placeholder"
confirm-type="search"
/>
<view v-if="searchText" class="search-clear" @click="clearSearch">
<uni-icons type="closeempty" size="18" color="#9ca3af"></uni-icons>
<view class="search-grid">
<view class="search-input-wrap">
<uni-icons type="search" size="18" color="#9ca3af"></uni-icons>
<input
v-model="searchCode"
class="search-input"
:placeholder="'\u7f16\u7801'"
placeholder-class="search-placeholder"
confirm-type="search"
@input="onCodeInput"
@confirm="handleSearch"
/>
<view v-if="searchCode" class="search-clear" @click="clearSearchCode">
<uni-icons type="closeempty" size="18" color="#9ca3af"></uni-icons>
</view>
</view>
<view class="search-input-wrap">
<uni-icons type="search" size="18" color="#9ca3af"></uni-icons>
<input
v-model="searchName"
class="search-input"
:placeholder="'\u540d\u79f0'"
placeholder-class="search-placeholder"
confirm-type="search"
@input="onNameInput"
@confirm="handleSearch"
/>
<view v-if="searchName" class="search-clear" @click="clearSearchName">
<uni-icons type="closeempty" size="18" color="#9ca3af"></uni-icons>
</view>
</view>
</view>
</view>
<scroll-view scroll-y class="product-list" v-if="filteredList.length">
<view
v-for="item in filteredList"
:key="item.id"
:class="['product-card', isSelected(item) ? 'active' : '']"
@click="selectedId = item.id"
>
<view v-for="item in filteredList" :key="getProductId(item)" :class="['product-card', isSelected(item) ? 'active' : '']" @click="selectedId = getProductId(item)">
<view class="product-header">
<view class="product-title-wrap">
<view class="product-name-row">
<text class="product-name">{{ textValue(item.name) }}</text>
<text v-if="getCategoryName(item)" class="category-tag">{{ getCategoryName(item) }}</text>
</view>
<text class="product-code">{{ textValue(item.barCode || item.code) }}</text>
<text class="product-name">{{ textValue(getProductName(item)) }}</text>
<text class="product-code">{{ textValue(getProductCode(item)) }}</text>
</view>
<view class="check-badge">
<uni-icons v-if="isSelected(item)" type="checkmarkempty" size="16" color="#ffffff"></uni-icons>
@ -39,12 +50,16 @@
</view>
<view class="info-grid">
<view class="info-cell">
<text class="info-label">{{ t('productInbound.spec') }}</text>
<text class="info-value">{{ textValue(item.standard || item.deviceSpec) }}</text>
<text class="info-label">{{ t('productInbound.packagingScheme') }}</text>
<text class="info-value">{{ textValue(getDefaultScheme(item)?.packagingSchemeName) }}</text>
</view>
<view class="info-cell">
<text class="info-label">{{ t('productInbound.unit') }}</text>
<text class="info-value">{{ textValue(item.unitName) }}</text>
<text class="info-label">{{ t('productInbound.palletPackageQuantity') }}</text>
<text class="info-value">{{ textValue(getDefaultScheme(item)?.palletPackageQuantity) }}</text>
</view>
<view class="info-cell">
<text class="info-label">{{ t('productInbound.packageQuantity') }}</text>
<text class="info-value">{{ textValue(getDefaultScheme(item)?.packageQuantity) }}</text>
</view>
</view>
</view>
@ -70,8 +85,15 @@ import { getProduct, getProductPage } from '@/api/erp/productInfo'
const { t } = useI18n()
const productList = ref([])
const selectedId = ref(null)
const searchText = ref('')
const searchCode = ref('')
const searchName = ref('')
const loading = ref(false)
const PRODUCT_CATEGORY_TYPE = 1
const PRODUCT_PAGE_SIZE = 10
const SEARCH_DELAY = 300
let searchTimer = null
const filteredList = computed(() => productList.value)
onLoad((options) => {
selectedId.value = options?.selectedId ? String(options.selectedId) : null
@ -83,59 +105,105 @@ function textValue(v) {
const s = String(v).trim()
return s || '-'
}
const filteredList = computed(() => {
const keyword = searchText.value.trim().toLowerCase()
if (!keyword) return productList.value
return productList.value.filter((item) =>
String(item.name || '').toLowerCase().includes(keyword) ||
String(item.barCode || item.code || '').toLowerCase().includes(keyword) ||
String(item.standard || item.deviceSpec || '').toLowerCase().includes(keyword) ||
String(getCategoryName(item)).toLowerCase().includes(keyword)
)
})
function isSelected(item) {
return String(selectedId.value) === String(item.id)
function normalizeValue(value) {
return String(value || '').trim().toLowerCase()
}
function getDefaultScheme(item) {
const schemes = Array.isArray(item?.packagingSchemes) ? item.packagingSchemes : []
return schemes.find((scheme) => Number(scheme?.defaultStatus) === 1) || schemes[0] || {}
}
function getProductId(item) {
return item?.id || item?.productId
}
function getProductName(item) {
return item?.name || item?.productName || ''
}
function getProductCode(item) {
return item?.barCode || item?.code || item?.productCode || ''
}
function getCategoryName(item) {
return item?.subCategoryName || item?.categoryName || item?.category?.name || ''
}
function clearSearch() {
searchText.value = ''
function isSelected(item) {
return String(selectedId.value) === String(getProductId(item))
}
function normalizePageList(res) {
const root = res && res.data !== undefined ? res.data : res
return Array.isArray(root) ? root : (root?.list || root?.rows || root?.records || [])
}
async function loadProducts() {
function getSearchParams(params = {}) {
return {
code: String((params.code ?? searchCode.value) || '').trim(),
name: String((params.name ?? searchName.value) || '').trim()
}
}
async function queryProducts(params = {}) {
const { code, name } = getSearchParams(params)
const queryParams = { pageNo: 1, pageSize: PRODUCT_PAGE_SIZE, categoryType: PRODUCT_CATEGORY_TYPE }
if (code) queryParams.barCode = code
if (name) queryParams.name = name
const res = await getProductPage(queryParams)
return normalizePageList(res)
}
async function loadProducts(params = {}) {
loading.value = true
try {
const raw = []
for (let pageNo = 1; pageNo <= 5; pageNo += 1) {
const res = await getProductPage({ pageNo, pageSize: 100, categoryType: 1 })
const list = normalizePageList(res)
if (!list.length) break
raw.push(...list)
if (list.length < 100) break
}
productList.value = raw
productList.value = await queryProducts(params)
} catch (e) {
uni.showToast({ title: t('productInbound.loadFailed'), icon: 'none' })
} finally {
loading.value = false
}
}
function handleSearch() {
if (searchTimer) {
clearTimeout(searchTimer)
searchTimer = null
}
loadProducts()
}
function scheduleSearch(params) {
if (searchTimer) clearTimeout(searchTimer)
searchTimer = setTimeout(() => {
searchTimer = null
loadProducts(params)
}, SEARCH_DELAY)
}
function onCodeInput(e) {
scheduleSearch({ code: e?.detail?.value ?? searchCode.value })
}
function onNameInput(e) {
scheduleSearch({ name: e?.detail?.value ?? searchName.value })
}
function clearSearchCode() {
if (searchTimer) {
clearTimeout(searchTimer)
searchTimer = null
}
searchCode.value = ''
loadProducts({ code: '' })
}
function clearSearchName() {
if (searchTimer) {
clearTimeout(searchTimer)
searchTimer = null
}
searchName.value = ''
loadProducts({ name: '' })
}
async function handleConfirm() {
if (!selectedId.value) {
uni.showToast({ title: t('productInbound.selectProduct'), icon: 'none' })
return
}
let item = productList.value.find((d) => String(d.id) === String(selectedId.value))
const item = productList.value.find((d) => String(getProductId(d)) === String(selectedId.value))
if (!item) return
let detail = item
try {
const res = await getProduct(item.id)
item = { ...item, ...(res?.data || res || {}) }
const res = await getProduct(getProductId(item))
detail = { ...item, ...(res?.data || res || {}) }
} catch (e) {}
getApp().globalData._productInboundProductSelectResult = item
getApp().globalData._productInboundProductSelectResult = detail
uni.navigateBack()
}
onShow(loadProducts)
@ -144,7 +212,8 @@ onShow(loadProducts)
<style lang="scss" scoped>
.page-container { min-height: 100vh; background: #f5f7fb; padding-bottom: calc(120rpx + env(safe-area-inset-bottom)); }
.search-bar { padding: 18rpx 24rpx; background: #ffffff; box-shadow: 0 4rpx 16rpx rgba(15, 23, 42, 0.03); }
.search-input-wrap { display: flex; align-items: center; gap: 12rpx; height: 76rpx; padding: 0 22rpx; background: #f8fafc; border: 1rpx solid #e5e7eb; border-radius: 14rpx; box-sizing: border-box; }
.search-grid { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 12rpx; }
.search-input-wrap { min-width: 0; display: flex; align-items: center; gap: 10rpx; height: 76rpx; padding: 0 18rpx; background: #f8fafc; border: 1rpx solid #e5e7eb; border-radius: 14rpx; box-sizing: border-box; }
.search-input { flex: 1; min-width: 0; font-size: 28rpx; color: #374151; }
.search-placeholder { color: #9ca3af; }
.search-clear { width: 44rpx; height: 44rpx; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
@ -153,13 +222,11 @@ onShow(loadProducts)
.product-card.active { border-color: #bfdbfe; background: #f9fbff; box-shadow: 0 8rpx 22rpx rgba(31, 124, 255, 0.08); }
.product-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 18rpx; }
.product-title-wrap { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 8rpx; }
.product-name-row { display: flex; align-items: center; gap: 12rpx; min-width: 0; }
.product-name { min-width: 0; font-size: 30rpx; font-weight: 600; color: #1f2937; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.category-tag { max-width: 180rpx; padding: 5rpx 14rpx; border-radius: 999rpx; background: #eff6ff; color: #1f7cff; font-size: 22rpx; line-height: 1.3; flex-shrink: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.product-name { font-size: 30rpx; font-weight: 600; color: #1f2937; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.product-code { font-size: 24rpx; color: #8a94a6; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.check-badge { width: 36rpx; height: 36rpx; border-radius: 18rpx; border: 1rpx solid #d1d5db; background: #ffffff; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
.product-card.active .check-badge { border-color: #1f7cff; background: #1f7cff; }
.info-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 14rpx; margin-top: 20rpx; padding-top: 18rpx; border-top: 1rpx solid #f1f5f9; }
.info-grid { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 14rpx; margin-top: 20rpx; padding-top: 18rpx; border-top: 1rpx solid #f1f5f9; }
.info-cell { min-width: 0; display: flex; flex-direction: column; gap: 6rpx; }
.info-label { font-size: 23rpx; color: #9ca3af; }
.info-value { font-size: 27rpx; color: #374151; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

@ -1,135 +0,0 @@
<template>
<view class="page-container">
<NavBar :title="t('productInbound.selectTaskProductTitle')" />
<view class="search-bar">
<view class="search-input-wrap">
<uni-icons type="search" size="18" color="#9ca3af"></uni-icons>
<input v-model="searchText" class="search-input" :placeholder="t('productInbound.searchTaskProductPlaceholder')" placeholder-class="search-placeholder" confirm-type="search" />
<view v-if="searchText" class="search-clear" @click="clearSearch">
<uni-icons type="closeempty" size="18" color="#9ca3af"></uni-icons>
</view>
</view>
</view>
<scroll-view scroll-y class="product-list" v-if="filteredList.length">
<view v-for="item in filteredList" :key="item.productId" :class="['product-card', isSelected(item) ? 'active' : '']" @click="selectedId = item.productId">
<view class="product-header">
<view class="product-title-wrap">
<text class="product-name">{{ textValue(item.productName) }}</text>
<text class="product-code">{{ textValue(item.productCode) }}</text>
</view>
<view class="check-badge">
<uni-icons v-if="isSelected(item)" type="checkmarkempty" size="16" color="#ffffff"></uni-icons>
</view>
</view>
<view class="info-grid">
<view class="info-cell">
<text class="info-label">{{ t('productInbound.packagingScheme') }}</text>
<text class="info-value">{{ textValue(getDefaultScheme(item)?.packagingSchemeName) }}</text>
</view>
<view class="info-cell">
<text class="info-label">{{ t('productInbound.palletPackageQuantity') }}</text>
<text class="info-value">{{ textValue(getDefaultScheme(item)?.palletPackageQuantity) }}</text>
</view>
<view class="info-cell">
<text class="info-label">{{ t('productInbound.packageQuantity') }}</text>
<text class="info-value">{{ textValue(getDefaultScheme(item)?.packageQuantity) }}</text>
</view>
</view>
</view>
</scroll-view>
<view v-else class="empty-wrap">
<uni-icons type="info" size="30" color="#cbd5e1"></uni-icons>
<text>{{ t('productInbound.emptyTaskProduct') }}</text>
</view>
<view class="action-bar">
<view :class="['action-btn', selectedId ? '' : 'action-btn-disabled']" @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'
const { t } = useI18n()
const productList = ref([])
const selectedId = ref(null)
const searchText = ref('')
const filteredList = computed(() => {
const keyword = searchText.value.trim().toLowerCase()
if (!keyword) return productList.value
return productList.value.filter((item) =>
String(item.productName || '').toLowerCase().includes(keyword) ||
String(item.productCode || '').toLowerCase().includes(keyword) ||
String(getDefaultScheme(item)?.packagingSchemeName || '').toLowerCase().includes(keyword)
)
})
onLoad((options) => {
selectedId.value = options?.selectedId ? String(options.selectedId) : null
})
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 isSelected(item) {
return String(selectedId.value) === String(item.productId)
}
function clearSearch() {
searchText.value = ''
}
function handleConfirm() {
if (!selectedId.value) {
uni.showToast({ title: t('productInbound.selectTaskProduct'), icon: 'none' })
return
}
const item = productList.value.find((d) => String(d.productId) === String(selectedId.value))
if (!item) return
getApp().globalData._productInboundTaskProductSelectResult = item
uni.navigateBack()
}
onShow(() => {
const list = getApp().globalData?._productInboundTaskProductList
productList.value = Array.isArray(list) ? list : []
})
</script>
<style lang="scss" scoped>
.page-container { min-height: 100vh; background: #f5f7fb; padding-bottom: calc(120rpx + env(safe-area-inset-bottom)); }
.search-bar { padding: 18rpx 24rpx; background: #ffffff; box-shadow: 0 4rpx 16rpx rgba(15, 23, 42, 0.03); }
.search-input-wrap { display: flex; align-items: center; gap: 12rpx; height: 76rpx; padding: 0 22rpx; background: #f8fafc; border: 1rpx solid #e5e7eb; border-radius: 14rpx; box-sizing: border-box; }
.search-input { flex: 1; min-width: 0; font-size: 28rpx; color: #374151; }
.search-placeholder { color: #9ca3af; }
.search-clear { width: 44rpx; height: 44rpx; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
.product-list { height: calc(100vh - 294rpx); }
.product-card { margin: 18rpx 24rpx 0; padding: 24rpx; background: #ffffff; border: 1rpx solid #eef2f7; border-radius: 20rpx; box-shadow: 0 6rpx 18rpx rgba(15, 23, 42, 0.04); }
.product-card.active { border-color: #bfdbfe; background: #f9fbff; box-shadow: 0 8rpx 22rpx rgba(31, 124, 255, 0.08); }
.product-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 18rpx; }
.product-title-wrap { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 8rpx; }
.product-name { font-size: 30rpx; font-weight: 600; color: #1f2937; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.product-code { font-size: 24rpx; color: #8a94a6; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.check-badge { width: 36rpx; height: 36rpx; border-radius: 18rpx; border: 1rpx solid #d1d5db; background: #ffffff; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
.product-card.active .check-badge { border-color: #1f7cff; background: #1f7cff; }
.info-grid { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 14rpx; margin-top: 20rpx; padding-top: 18rpx; border-top: 1rpx solid #f1f5f9; }
.info-cell { min-width: 0; display: flex; flex-direction: column; gap: 6rpx; }
.info-label { font-size: 23rpx; color: #9ca3af; }
.info-value { font-size: 27rpx; color: #374151; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.empty-wrap { height: calc(100vh - 294rpx); display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 14rpx; color: #94a3b8; font-size: 27rpx; }
.action-bar { position: fixed; left: 0; right: 0; bottom: 0; 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 { height: 84rpx; border-radius: 16rpx; background: #1f4b79; color: #ffffff; display: flex; align-items: center; justify-content: center; font-size: 30rpx; font-weight: 600; }
.action-btn-disabled { background: #94a3b8; }
</style>
Loading…
Cancel
Save