style:产品、物料、备件交互优化

master
黄伟杰 3 days ago
parent e97542bda4
commit 723367cf43

@ -17,8 +17,7 @@
v-for="item in filteredList" v-for="item in filteredList"
:key="item.id" :key="item.id"
class="material-card" class="material-card"
:class="{ active: selectedId === item.id }" @tap="handleSelectItem(item)"
@click="selectedId = item.id"
> >
<view class="material-card-header"> <view class="material-card-header">
<text class="material-name">{{ textValue(item.name) }}</text> <text class="material-name">{{ textValue(item.name) }}</text>
@ -57,13 +56,6 @@
<text v-if="loading" class="empty-text">...</text> <text v-if="loading" class="empty-text">...</text>
<text v-else class="empty-text">暂无物料数据</text> <text v-else class="empty-text">暂无物料数据</text>
</view> </view>
<!-- 底部确认按钮 -->
<view class="bottom-actions">
<view class="bottom-btn confirm-btn" @click="handleConfirm">
确认
</view>
</view>
</view> </view>
</template> </template>
@ -74,7 +66,6 @@ import NavBar from '@/components/common/NavBar.vue'
import { getMaterialSimpleList } from '@/api/mes/sparepart' import { getMaterialSimpleList } from '@/api/mes/sparepart'
const materialList = ref([]) const materialList = ref([])
const selectedId = ref(null)
const searchText = ref('') const searchText = ref('')
const loading = ref(false) const loading = ref(false)
const fromSource = ref('inbound') const fromSource = ref('inbound')
@ -135,12 +126,7 @@ async function loadMaterials() {
} }
} }
function handleConfirm() { function handleSelectItem(item) {
if (!selectedId.value) {
uni.showToast({ title: '请选择物料', icon: 'none' })
return
}
const item = materialList.value.find((d) => d.id === selectedId.value)
if (!item) return if (!item) return
getApp().globalData._materialBeforeConfirm = item getApp().globalData._materialBeforeConfirm = item
const from = fromSource.value || getApp().globalData._materialSelectFrom || 'inbound' const from = fromSource.value || getApp().globalData._materialSelectFrom || 'inbound'
@ -148,7 +134,11 @@ function handleConfirm() {
? '/pages_function/pages/materialOutbound/materialConfirm' ? '/pages_function/pages/materialOutbound/materialConfirm'
: '/pages_function/pages/materialInbound/materialConfirm' : '/pages_function/pages/materialInbound/materialConfirm'
uni.navigateTo({ uni.navigateTo({
url url,
fail: (err) => {
console.error('[materialSelect] navigateTo failed', err)
uni.showToast({ title: err?.errMsg || 'Navigate failed', icon: 'none' })
}
}) })
} }
@ -165,7 +155,6 @@ onShow(async () => {
.page-container { .page-container {
min-height: 100vh; min-height: 100vh;
background: #f5f6f8; background: #f5f6f8;
padding-bottom: 140rpx;
} }
/* 搜索栏 */ /* 搜索栏 */
@ -216,11 +205,6 @@ onShow(async () => {
padding: 24rpx; padding: 24rpx;
margin-bottom: 16rpx; margin-bottom: 16rpx;
border: 2rpx solid transparent; border: 2rpx solid transparent;
&.active {
border-color: #2563eb;
background: #f0f5ff;
}
} }
.material-card-header { .material-card-header {
@ -273,31 +257,4 @@ onShow(async () => {
font-size: 28rpx; font-size: 28rpx;
color: #999; color: #999;
} }
/* 底部确认按钮 */
.bottom-actions {
position: fixed;
left: 0;
right: 0;
bottom: 0;
padding: 18rpx 24rpx calc(18rpx + env(safe-area-inset-bottom));
background: #fff;
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.06);
z-index: 99;
}
.bottom-btn {
width: 100%;
height: 84rpx;
line-height: 84rpx;
text-align: center;
border-radius: 16rpx;
font-size: 30rpx;
font-weight: 600;
}
.confirm-btn {
background: #1f4b79;
color: #fff;
}
</style> </style>

@ -32,22 +32,37 @@
<view v-if="relateTask && taskId" class="form-field"> <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.taskProduct') }}<text class="required-star">*</text></text>
<view :class="['select-field', selectedTaskProduct ? 'selected' : '']" @click="goSelectTaskProduct"> <view class="task-product-search-row">
<view class="select-content"> <input
<text :class="selectedTaskProduct ? 'select-value' : 'select-placeholder'">{{ selectedTaskProduct ? textValue(selectedTaskProduct.productName) : t('productInbound.selectTaskProduct') }}</text> id="product-inbound-task-product-scan-input"
v-model="taskProductScanInput"
class="task-product-scan-input"
type="text"
:placeholder="t('productInbound.searchTaskProductPlaceholder')"
confirm-type="done"
@confirm="onTaskProductScanConfirm"
/>
<view class="task-product-select-field" @click="goSelectTaskProduct">
<text class="task-product-select-text">{{ t('productInbound.selectProductTitle') }}</text>
</view> </view>
<uni-icons type="right" size="18" color="#9ca3af"></uni-icons>
</view> </view>
</view> </view>
<view v-if="!relateTask" class="form-field"> <view v-if="!relateTask" class="form-field">
<text class="form-label">{{ t('productInbound.product') }}<text class="required-star">*</text></text> <text class="form-label">{{ t('productInbound.product') }}<text class="required-star">*</text></text>
<view :class="['select-field', productName ? 'selected' : '']" @click="goSelectProduct"> <view class="task-product-search-row">
<view class="select-content"> <input
<text :class="productName ? 'select-value' : 'select-placeholder'">{{ productName || t('productInbound.selectProduct') }}</text> id="product-inbound-product-scan-input"
<text v-if="productBarCode" class="select-subtext">{{ textValue(productBarCode) }}</text> v-model="productScanInput"
class="task-product-scan-input"
type="text"
:placeholder="t('productInbound.searchProductPlaceholder')"
confirm-type="done"
@confirm="onProductScanConfirm"
/>
<view class="task-product-select-field" @click="goSelectProduct">
<text class="task-product-select-text">{{ t('productInbound.selectProductTitle') }}</text>
</view> </view>
<uni-icons type="right" size="18" color="#9ca3af"></uni-icons>
</view> </view>
</view> </view>
@ -172,6 +187,7 @@ import { onLoad, onShow } from '@dcloudio/uni-app'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import NavBar from '@/components/common/NavBar.vue' import NavBar from '@/components/common/NavBar.vue'
import { getTaskDefaultPackagingSchemes } from '@/api/mes/productInbound' import { getTaskDefaultPackagingSchemes } from '@/api/mes/productInbound'
import { getProduct, getProductPage } from '@/api/erp/productInfo'
import { getWarehouseAreaSimpleList, getWarehouseSimpleList } from '@/api/mes/moldget' import { getWarehouseAreaSimpleList, getWarehouseSimpleList } from '@/api/mes/moldget'
const { t } = useI18n() const { t } = useI18n()
@ -183,6 +199,8 @@ const selectedTaskData = ref(null)
const selectedTaskProduct = ref(null) const selectedTaskProduct = ref(null)
const selectedPallets = ref([]) const selectedPallets = ref([])
const editingIndex = ref(null) const editingIndex = ref(null)
const taskProductScanInput = ref('')
const productScanInput = ref('')
const warehouseOptions = ref([]) const warehouseOptions = ref([])
const warehouseAreaMap = ref({}) const warehouseAreaMap = ref({})
@ -337,6 +355,8 @@ function setRelateTask(value) {
relateTask.value = value relateTask.value = value
product.value = {} product.value = {}
selectedTaskProduct.value = null selectedTaskProduct.value = null
taskProductScanInput.value = ''
productScanInput.value = ''
taskId.value = null taskId.value = null
taskCode.value = '' taskCode.value = ''
selectedTaskData.value = null selectedTaskData.value = null
@ -363,6 +383,109 @@ function goSelectTaskProduct() {
const suffix = selectedTaskProduct.value?.productId ? `?selectedId=${selectedTaskProduct.value.productId}` : '' 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/taskProductSelect${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 getTaskProductScanValues(item) {
return [
item?.productId,
item?.productCode,
item?.productBarCode,
item?.barCode,
item?.code,
item?.productName,
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) {
try {
const res = await getProduct(productId)
const detail = res?.data || res
if (detail?.id) return 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
}
}
if (list.length < 100) break
}
return null
}
async function onProductScanConfirm() {
const keywords = getProductScanKeywords(productScanInput.value)
if (!keywords.length) return
try {
uni.showLoading({ title: t('productInbound.loading'), mask: true })
const matched = await findProductByScanCode(productScanInput.value)
uni.hideLoading()
if (!matched?.id) {
uni.showToast({ title: t('productInbound.emptyProduct'), icon: 'none' })
return
}
product.value = matched
selectedTaskProduct.value = null
selectedPallets.value = []
productScanInput.value = ''
} catch (e) {
uni.hideLoading()
uni.showToast({ title: t('productInbound.loadFailed'), icon: 'none' })
}
}
function goSelectPallet() { function goSelectPallet() {
if (!productId.value) { if (!productId.value) {
uni.showToast({ title: t('productInbound.selectProductFirst'), icon: 'none' }) uni.showToast({ title: t('productInbound.selectProductFirst'), icon: 'none' })
@ -496,6 +619,7 @@ onShow(async () => {
taskId.value = taskResult.id taskId.value = taskResult.id
taskCode.value = taskResult.code || taskResult.taskName || '' taskCode.value = taskResult.code || taskResult.taskName || ''
selectedTaskProduct.value = null selectedTaskProduct.value = null
taskProductScanInput.value = ''
selectedPallets.value = [] selectedPallets.value = []
gd._productInboundTaskSelectResult = null gd._productInboundTaskSelectResult = null
try { try {
@ -512,12 +636,14 @@ onShow(async () => {
const taskProductResult = gd?._productInboundTaskProductSelectResult const taskProductResult = gd?._productInboundTaskProductSelectResult
if (taskProductResult) { if (taskProductResult) {
selectedTaskProduct.value = taskProductResult selectedTaskProduct.value = taskProductResult
taskProductScanInput.value = ''
selectedPallets.value = [] selectedPallets.value = []
gd._productInboundTaskProductSelectResult = null gd._productInboundTaskProductSelectResult = null
} }
const productResult = gd?._productInboundProductSelectResult const productResult = gd?._productInboundProductSelectResult
if (productResult) { if (productResult) {
product.value = productResult product.value = productResult
productScanInput.value = ''
selectedPallets.value = [] selectedPallets.value = []
gd._productInboundProductSelectResult = null gd._productInboundProductSelectResult = null
} }
@ -554,6 +680,10 @@ onShow(async () => {
.select-value { font-size: 28rpx; font-weight: 600; color: #1f2937; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .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-placeholder { font-size: 28rpx; color: #9ca3af; }
.select-subtext { font-size: 24rpx; color: #6b7280; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .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; }
.task-product-scan-input { flex: 1; min-width: 0; height: 70rpx; padding: 0 22rpx; background: #f8fafc; border: 1rpx solid #e5e7eb; border-radius: 14rpx; box-sizing: border-box; font-size: 28rpx; color: #1f2937; }
.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-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-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 { min-width: 0; display: flex; flex-direction: column; gap: 8rpx; padding: 18rpx 20rpx; border-right: 1rpx solid #f1f5f9; border-bottom: 1rpx solid #f1f5f9; }

@ -2,6 +2,22 @@
<view class="page-container"> <view class="page-container">
<NavBar :title="t('productOutbound.createTitle')" /> <NavBar :title="t('productOutbound.createTitle')" />
<view class="top-action-bar">
<view class="scan-input-row">
<input
id="product-outbound-scan-input"
class="scan-input"
v-model="scanCodeInput"
placeholder="&#32418;&#22806;&#25195;&#30721;&#25110;&#36755;&#20837;&#20135;&#21697;&#30721;"
confirm-type="done"
@confirm="onScanInputConfirm"
/>
</view>
<view class="scan-btn" @click="handleAddProduct">
<text class="btn-text">&#36873;&#25321;&#20135;&#21697;</text>
</view>
</view>
<scroll-view scroll-y class="detail-scroll"> <scroll-view scroll-y class="detail-scroll">
<view class="content-section"> <view class="content-section">
<view class="section-card"> <view class="section-card">
@ -134,15 +150,17 @@
<view class="action-btn back-btn" @click="handleCancel">{{ t('productOutbound.cancel') }}</view> <view class="action-btn back-btn" @click="handleCancel">{{ t('productOutbound.cancel') }}</view>
<view class="action-btn submit-btn" @click="handleSubmit">{{ t('productOutbound.confirmOutbound') }}</view> <view class="action-btn submit-btn" @click="handleSubmit">{{ t('productOutbound.confirmOutbound') }}</view>
</view> </view>
<sv-focus-no-keyboard ref="focusNoKeyboardRef"></sv-focus-no-keyboard>
</view> </view>
</template> </template>
<script setup> <script setup>
import { computed, ref } from 'vue' import { computed, ref, nextTick } from 'vue'
import { onShow } from '@dcloudio/uni-app' import { onReady, onShow } from '@dcloudio/uni-app'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import NavBar from '@/components/common/NavBar.vue' import NavBar from '@/components/common/NavBar.vue'
import { createProductOutbound } from '@/api/mes/productOutbound' import { createProductOutbound } from '@/api/mes/productOutbound'
import { getProduct, getProductPage } from '@/api/erp/productInfo'
import { setDefaultOperatorFromCurrentUser } from '@/utils/currentUser' import { setDefaultOperatorFromCurrentUser } from '@/utils/currentUser'
const { t } = useI18n() const { t } = useI18n()
@ -152,6 +170,9 @@ const selectedOperatorId = ref(null)
const selectedOperatorName = ref('') const selectedOperatorName = ref('')
const remark = ref('') const remark = ref('')
const attachmentList = ref([]) const attachmentList = ref([])
const scanCodeInput = ref('')
const focusNoKeyboardRef = ref(null)
const keywordInputSelector = '#product-outbound-scan-input input, input#product-outbound-scan-input'
const totalPalletCount = computed(() => itemList.value.reduce((sum, item) => sum + getPalletCount(item), 0)) const totalPalletCount = computed(() => itemList.value.reduce((sum, item) => sum + getPalletCount(item), 0))
const totalPackageCount = computed(() => itemList.value.reduce((sum, item) => sum + (Number(item.packageCount) || 0), 0)) const totalPackageCount = computed(() => itemList.value.reduce((sum, item) => sum + (Number(item.packageCount) || 0), 0))
@ -172,17 +193,79 @@ function textValue(v) {
function handleDateChange(e) { function handleDateChange(e) {
outboundDate.value = e.detail.value outboundDate.value = e.detail.value
} }
function focusKeywordNoKeyboard() {
nextTick(() => {
setTimeout(() => {
focusNoKeyboardRef.value?.focus(keywordInputSelector)
}, 80)
})
}
function handleAddProduct() { function handleAddProduct() {
const gd = getApp().globalData const gd = getApp().globalData
gd._productOutboundEditIndex = null gd._productOutboundEditIndex = null
gd._productOutboundEditItem = null gd._productOutboundEditItem = null
gd._productOutboundItems = [...itemList.value] gd._productOutboundItems = [...itemList.value]
uni.navigateTo({ url: '/pages_function/pages/productOutbound/productConfirm' }) uni.navigateTo({ url: '/pages_function/pages/productOutbound/productSelect?from=create' })
} }
function goSelectOperator() { function goSelectOperator() {
getApp().globalData._productOutboundUserFrom = 'outbound' getApp().globalData._productOutboundUserFrom = 'outbound'
uni.navigateTo({ url: '/pages_function/pages/moldRepair/userSelect?field=operator&from=productOutbound' }) uni.navigateTo({ url: '/pages_function/pages/moldRepair/userSelect?field=operator&from=productOutbound' })
} }
function onScanInputConfirm() {
const code = scanCodeInput.value.trim()
if (!code) return
handleScanCode(code)
}
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(code) {
const rawCode = String(code || '').trim()
const productId = rawCode.toUpperCase().startsWith('PRODUCT-')
? rawCode.replace(/PRODUCT-/i, '')
: (/^\d+$/.test(rawCode) ? rawCode : null)
if (productId) {
try {
const res = await getProduct(productId)
const detail = res?.data || res
if (detail?.id) return 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) =>
String(item.id || '') === rawCode ||
String(item.barCode || '') === rawCode ||
String(item.code || '') === rawCode
)
if (matched) {
try {
const detailRes = await getProduct(matched.id)
return { ...matched, ...(detailRes?.data || detailRes || {}) }
} catch (e) {
return matched
}
}
if (list.length < 100) break
}
return null
}
async function handleScanCode(code) {
try {
const detail = await findProductByScanCode(code)
if (detail?.id) {
getApp().globalData._productOutboundProductSelectResult = detail
uni.navigateTo({ url: '/pages_function/pages/productOutbound/productConfirm' })
} else {
uni.showToast({ title: '\u672a\u627e\u5230\u4ea7\u54c1: ' + code, icon: 'none' })
}
} catch (e) {
console.error('[productOutbound] scan failed', e)
uni.showToast({ title: '\u626b\u7801\u5931\u8d25', icon: 'none' })
}
}
function outModeText(value) { function outModeText(value) {
if (Number(value) === 1) return t('productOutbound.outModeWholePallet') if (Number(value) === 1) return t('productOutbound.outModeWholePallet')
if (Number(value) === 2) return t('productOutbound.outModeSplitPallet') if (Number(value) === 2) return t('productOutbound.outModeSplitPallet')
@ -325,6 +408,10 @@ async function handleSubmit() {
} }
} }
onReady(() => {
focusKeywordNoKeyboard()
})
onShow(() => { onShow(() => {
const items = getApp().globalData?._productOutboundItems const items = getApp().globalData?._productOutboundItems
if (Array.isArray(items)) itemList.value = [...items] if (Array.isArray(items)) itemList.value = [...items]
@ -339,7 +426,12 @@ onShow(() => {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.page-container { min-height: 100vh; background: #f5f7fb; } .page-container { min-height: 100vh; background: #f5f7fb; }
.detail-scroll { height: calc(100vh - 172rpx); } .top-action-bar { display: flex; align-items: center; gap: 16rpx; padding: 20rpx 24rpx; background: #ffffff; border-bottom: 1rpx solid #eef2f7; }
.scan-input-row { flex: 1; }
.scan-input { width: 100%; height: 76rpx; padding: 0 20rpx; font-size: 26rpx; color: #333; background: #f8fafc; border: 1rpx solid #e5e7eb; border-radius: 14rpx; box-sizing: border-box; }
.scan-btn { display: flex; align-items: center; justify-content: center; height: 76rpx; padding: 0 24rpx; border-radius: 14rpx; background: #1f4b79; white-space: nowrap; flex-shrink: 0; }
.btn-text { font-size: 26rpx; font-weight: 600; color: #fff; }
.detail-scroll { height: calc(100vh - 172rpx - 116rpx); }
.content-section { padding: 20rpx 24rpx 28rpx; } .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-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-header { display: flex; align-items: center; gap: 12rpx; margin-bottom: 22rpx; padding-bottom: 18rpx; border-bottom: 1rpx solid #f1f5f9; }

@ -12,17 +12,6 @@
<text class="section-title">{{ t('productOutbound.productInfo') }}</text> <text class="section-title">{{ t('productOutbound.productInfo') }}</text>
</view> </view>
<view class="form-field">
<text class="form-label">{{ t('productOutbound.product') }}<text class="required-star">*</text></text>
<view :class="['select-field', productName ? 'selected' : '']" @click="goSelectProduct">
<view class="select-content">
<text :class="productName ? 'select-value' : 'select-placeholder'">{{ productName || t('productOutbound.selectProduct') }}</text>
<text v-if="productBarCode" class="select-subtext">{{ textValue(productBarCode) }}</text>
</view>
<uni-icons type="right" size="18" color="#9ca3af"></uni-icons>
</view>
</view>
<view v-if="productId" class="info-panel"> <view v-if="productId" class="info-panel">
<view class="info-grid"> <view class="info-grid">
<view class="info-item"> <view class="info-item">
@ -151,6 +140,7 @@ const product = ref({})
const outMode = ref(1) const outMode = ref(1)
const selectedPallets = ref([]) const selectedPallets = ref([])
const editingIndex = ref(null) const editingIndex = ref(null)
const fromSelectPage = ref(false)
const warehouseOptions = ref([]) const warehouseOptions = ref([])
const warehouseAreaMap = ref({}) const warehouseAreaMap = ref({})
@ -246,10 +236,6 @@ function setOutMode(value) {
packageCount: value === 1 ? getAvailablePackageCount(pallet) : Math.min(Number(pallet.packageCount) || 0, getAvailablePackageCount(pallet)) packageCount: value === 1 ? getAvailablePackageCount(pallet) : Math.min(Number(pallet.packageCount) || 0, getAvailablePackageCount(pallet))
})) }))
} }
function goSelectProduct() {
const suffix = product.value?.id ? `?selectedId=${product.value.id}` : ''
uni.navigateTo({ url: `/pages_function/pages/productOutbound/productSelect${suffix}` })
}
function goSelectPallet() { function goSelectPallet() {
if (!productId.value) { if (!productId.value) {
uni.showToast({ title: t('productOutbound.selectProductFirst'), icon: 'none' }) uni.showToast({ title: t('productOutbound.selectProductFirst'), icon: 'none' })
@ -367,10 +353,11 @@ function handleConfirm() {
gd._productOutboundEditIndex = null gd._productOutboundEditIndex = null
gd._productOutboundEditItem = null gd._productOutboundEditItem = null
uni.showToast({ title: t('productOutbound.productAdded'), icon: 'success', duration: 1000 }) uni.showToast({ title: t('productOutbound.productAdded'), icon: 'success', duration: 1000 })
setTimeout(() => uni.navigateBack(), 700) setTimeout(() => uni.navigateBack({ delta: fromSelectPage.value ? 2 : 1 }), 700)
} }
onLoad(async () => { onLoad(async (options) => {
fromSelectPage.value = options?.fromSelect === '1'
const gd = getApp().globalData const gd = getApp().globalData
const index = gd?._productOutboundEditIndex const index = gd?._productOutboundEditIndex
const item = gd?._productOutboundEditItem const item = gd?._productOutboundEditItem

@ -22,8 +22,8 @@
<view <view
v-for="item in filteredList" v-for="item in filteredList"
:key="item.id" :key="item.id"
:class="['product-card', isSelected(item) ? 'active' : '']" class="product-card"
@click="selectedId = item.id" @tap="handleSelectProduct(item)"
> >
<view class="product-header"> <view class="product-header">
<view class="product-title-wrap"> <view class="product-title-wrap">
@ -33,9 +33,6 @@
</view> </view>
<text class="product-code">{{ textValue(item.barCode || item.code) }}</text> <text class="product-code">{{ textValue(item.barCode || item.code) }}</text>
</view> </view>
<view class="check-badge">
<uni-icons v-if="isSelected(item)" type="checkmarkempty" size="16" color="#ffffff"></uni-icons>
</view>
</view> </view>
<view class="info-grid"> <view class="info-grid">
<view class="info-cell"> <view class="info-cell">
@ -53,10 +50,6 @@
<uni-icons type="info" size="30" color="#cbd5e1"></uni-icons> <uni-icons type="info" size="30" color="#cbd5e1"></uni-icons>
<text>{{ loading ? t('productOutbound.loading') : t('productOutbound.emptyProduct') }}</text> <text>{{ loading ? t('productOutbound.loading') : t('productOutbound.emptyProduct') }}</text>
</view> </view>
<view class="action-bar">
<view :class="['action-btn', selectedId ? '' : 'action-btn-disabled']" @click="handleConfirm">{{ t('productOutbound.confirm') }}</view>
</view>
</view> </view>
</template> </template>
@ -69,12 +62,12 @@ import { getProduct, getProductPage } from '@/api/erp/productInfo'
const { t } = useI18n() const { t } = useI18n()
const productList = ref([]) const productList = ref([])
const selectedId = ref(null) const fromSource = ref('create')
const searchText = ref('') const searchText = ref('')
const loading = ref(false) const loading = ref(false)
onLoad((options) => { onLoad((options) => {
selectedId.value = options?.selectedId ? String(options.selectedId) : null fromSource.value = String(options?.from || 'create')
}) })
function textValue(v) { function textValue(v) {
@ -93,9 +86,6 @@ const filteredList = computed(() => {
String(getCategoryName(item)).toLowerCase().includes(keyword) String(getCategoryName(item)).toLowerCase().includes(keyword)
) )
}) })
function isSelected(item) {
return String(selectedId.value) === String(item.id)
}
function getCategoryName(item) { function getCategoryName(item) {
return item?.subCategoryName || item?.categoryName || item?.category?.name || '' return item?.subCategoryName || item?.categoryName || item?.category?.name || ''
} }
@ -124,47 +114,41 @@ async function loadProducts() {
loading.value = false loading.value = false
} }
} }
async function handleConfirm() { async function handleSelectProduct(item) {
if (!selectedId.value) {
uni.showToast({ title: t('productOutbound.selectProduct'), icon: 'none' })
return
}
let item = productList.value.find((d) => String(d.id) === String(selectedId.value))
if (!item) return if (!item) return
let nextItem = item
try { try {
const res = await getProduct(item.id) const res = await getProduct(item.id)
item = { ...item, ...(res?.data || res || {}) } nextItem = { ...item, ...(res?.data || res || {}) }
} catch (e) {} } catch (e) {}
getApp().globalData._productOutboundProductSelectResult = item getApp().globalData._productOutboundProductSelectResult = nextItem
if (fromSource.value === 'confirm') {
uni.navigateBack() uni.navigateBack()
return
}
uni.navigateTo({ url: '/pages_function/pages/productOutbound/productConfirm?fromSelect=1' })
} }
onShow(loadProducts) onShow(loadProducts)
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.page-container { min-height: 100vh; background: #f5f7fb; padding-bottom: calc(120rpx + env(safe-area-inset-bottom)); } .page-container { min-height: 100vh; background: #f5f7fb; }
.search-bar { padding: 18rpx 24rpx; background: #ffffff; box-shadow: 0 4rpx 16rpx rgba(15, 23, 42, 0.03); } .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-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-input { flex: 1; min-width: 0; font-size: 28rpx; color: #374151; }
.search-placeholder { color: #9ca3af; } .search-placeholder { color: #9ca3af; }
.search-clear { width: 44rpx; height: 44rpx; display: flex; align-items: center; justify-content: center; flex-shrink: 0; } .search-clear { width: 44rpx; height: 44rpx; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
.product-list { height: calc(100vh - 294rpx); } .product-list { height: calc(100vh - 194rpx); padding-bottom: 24rpx; box-sizing: border-box; }
.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 { 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-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-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-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; } .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; } .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-code { font-size: 24rpx; color: #8a94a6; 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; 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-cell { min-width: 0; display: flex; flex-direction: column; gap: 6rpx; }
.info-label { font-size: 23rpx; color: #9ca3af; } .info-label { font-size: 23rpx; color: #9ca3af; }
.info-value { font-size: 27rpx; color: #374151; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .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; } .empty-wrap { height: calc(100vh - 194rpx); 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> </style>

@ -17,8 +17,7 @@
v-for="item in filteredList" v-for="item in filteredList"
:key="item.id" :key="item.id"
class="sparepart-card" class="sparepart-card"
:class="{ active: selectedId === item.id }" @tap="handleSelectItem(item)"
@click="selectedId = item.id"
> >
<view class="sparepart-card-header"> <view class="sparepart-card-header">
<text class="sparepart-name">{{ textValue(item.name) }}</text> <text class="sparepart-name">{{ textValue(item.name) }}</text>
@ -57,13 +56,6 @@
<text v-if="loading" class="empty-text">{{ t('functionCommon.loading') }}</text> <text v-if="loading" class="empty-text">{{ t('functionCommon.loading') }}</text>
<text v-else class="empty-text">{{ t('sparepartInbound.noSparepartData') }}</text> <text v-else class="empty-text">{{ t('sparepartInbound.noSparepartData') }}</text>
</view> </view>
<!-- 底部确认按钮 -->
<view class="bottom-actions">
<view class="bottom-btn confirm-btn" @click="handleConfirm">
{{ t('functionCommon.confirm') }}
</view>
</view>
</view> </view>
</template> </template>
@ -77,7 +69,6 @@ import { getSparepartSimpleList } from '@/api/mes/sparepart'
const { t } = useI18n() const { t } = useI18n()
const sparepartList = ref([]) const sparepartList = ref([])
const selectedId = ref(null)
const searchText = ref('') const searchText = ref('')
const loading = ref(false) const loading = ref(false)
@ -137,20 +128,20 @@ async function loadSpareparts() {
} }
} }
function handleConfirm() { function handleSelectItem(item) {
if (!selectedId.value) {
uni.showToast({ title: t('sparepartInbound.validatorSparepartRequired'), icon: 'none' })
return
}
const item = sparepartList.value.find((d) => d.id === selectedId.value)
if (!item) return if (!item) return
// globalData
getApp().globalData._sparepartBeforeConfirm = item getApp().globalData._sparepartBeforeConfirm = item
const from = getApp().globalData._sparepartSelectFrom || 'inbound' const from = getApp().globalData._sparepartSelectFrom || 'inbound'
const url = from === 'outbound' const url = from === 'outbound'
? '/pages_function/pages/sparepartOutbound/sparepartConfirm' ? '/pages_function/pages/sparepartOutbound/sparepartConfirm'
: '/pages_function/pages/sparepartInbound/sparepartConfirm' : '/pages_function/pages/sparepartInbound/sparepartConfirm'
uni.navigateTo({ url }) uni.navigateTo({
url,
fail: (err) => {
console.error('[sparepartSelect] navigateTo failed', err)
uni.showToast({ title: err?.errMsg || 'Navigate failed', icon: 'none' })
}
})
} }
onShow(async () => { onShow(async () => {
@ -162,7 +153,6 @@ onShow(async () => {
.page-container { .page-container {
min-height: 100vh; min-height: 100vh;
background: #f5f6f8; background: #f5f6f8;
padding-bottom: 140rpx;
} }
/* 搜索栏 */ /* 搜索栏 */
@ -213,11 +203,6 @@ onShow(async () => {
padding: 24rpx; padding: 24rpx;
margin-bottom: 16rpx; margin-bottom: 16rpx;
border: 2rpx solid transparent; border: 2rpx solid transparent;
&.active {
border-color: #2563eb;
background: #f0f5ff;
}
} }
.sparepart-card-header { .sparepart-card-header {
@ -270,31 +255,4 @@ onShow(async () => {
font-size: 28rpx; font-size: 28rpx;
color: #999; color: #999;
} }
/* 底部确认按钮 */
.bottom-actions {
position: fixed;
left: 0;
right: 0;
bottom: 0;
padding: 18rpx 24rpx calc(18rpx + env(safe-area-inset-bottom));
background: #fff;
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.06);
z-index: 99;
}
.bottom-btn {
width: 100%;
height: 84rpx;
line-height: 84rpx;
text-align: center;
border-radius: 16rpx;
font-size: 30rpx;
font-weight: 600;
}
.confirm-btn {
background: #1f4b79;
color: #fff;
}
</style> </style>

Loading…
Cancel
Save