add:PDA配盘入库功能

This commit is contained in:
zhaoyf
2026-06-16 17:16:02 +08:00
parent a9efd88267
commit 34a6c03720
6 changed files with 482 additions and 8 deletions

View File

@@ -96,13 +96,14 @@
<insert id="insertAllData">
INSERT INTO md_pb_groupplate(
group_id,storagevehicle_code,material_id,pcsn,qty,qty_unit_id,qty_unit_name,ext_code,load_port,create_id,create_name,create_time
group_id,storagevehicle_code,material_id,material_code,pcsn,qty,qty_unit_id,qty_unit_name,ext_code,load_port,create_id,create_name,create_time
) values
<foreach collection="data" item="item" separator=",">
(
#{item.group_id},
#{item.storagevehicle_code},
#{item.material_id},
#{item.material_code},
#{item.pcsn},
#{item.qty},
#{item.qty_unit_id},

View File

@@ -58,6 +58,7 @@ public class ProductGroupServiceImpl implements IProductGroupService {
json.put("load_port", dto.getLoadPort());
json.put("storagevehicle_code", dto.getPalletCode());
json.put("material_id", bomData.getMaterialCode());
json.put("material_code", bomData.getMaterialCode());
json.put("pcsn", "");
json.put("qty_unit_id", bomData.getUnit());
json.put("qty_unit_name", bomData.getUnit());

View File

@@ -26,7 +26,8 @@ const zh = {
'平库库存查询',
'库位绑定/解绑',
'平库调拨出库',
'线边领料出库'
'线边领料出库',
'配盘出库',
// '空托上架/下架/注册/呼叫',
// 'AGV配送',
// '人工盘点',
@@ -148,6 +149,35 @@ const zh = {
complete: '完成',
completeSuccess: '操作成功',
},
productGroup: {
title: '配盘出库',
workSect: '工作区',
workSectPlaceholder: '请选择工作区',
workSectRequired: '请选择工作区',
palletCode: '托盘码',
palletCodePlaceholder: '请扫描或输入托盘码',
palletCodeRequired: '请输入托盘码',
workOrder: '工单',
workOrderPlaceholder: '请选择工单',
workOrderRequired: '请选择工单',
loadPort: '上料口',
loadPortPlaceholder: '请选择上料口',
loadPortRequired: '请选择上料口',
total: '共{0}条',
noData: '暂无物料',
noOrder: '暂无工单',
noPort: '请先选择工单',
materialCode: '物料编码',
materialName: '物料名称',
bomQty: '需求数量',
useBomQty: '已用数量',
planQty: '待配数量',
inputQty: '配盘数量',
inputQtyPlaceholder: '请输入数量',
unit: '单位',
confirm: '确定',
submitSuccess: '提交成功',
},
}
const en = {
@@ -175,8 +205,9 @@ const en = {
'Putaway',
'Inventory Query',
'Bind/Unbind',
'Picking Hall',
'Containerless Receive',
'Flat Outbound',
'Line Picking',
'Product Group',
'Empty Pallet Ops',
'AGV Delivery',
'Manual Inventory',
@@ -299,6 +330,35 @@ const en = {
complete: 'Complete',
completeSuccess: 'Success',
},
productGroup: {
title: 'Product Group',
workSect: 'Work Area',
workSectPlaceholder: 'Select work area',
workSectRequired: 'Work area is required',
palletCode: 'Pallet Code',
palletCodePlaceholder: 'Scan or enter pallet code',
palletCodeRequired: 'Pallet code is required',
workOrder: 'Work Order',
workOrderPlaceholder: 'Select work order',
workOrderRequired: 'Work order is required',
loadPort: 'Load Port',
loadPortPlaceholder: 'Select load port',
loadPortRequired: 'Load port is required',
total: 'Total {0} items',
noData: 'No materials',
noOrder: 'No work orders',
noPort: 'Please select work order first',
materialCode: 'Material Code',
materialName: 'Material Name',
bomQty: 'BOM Qty',
useBomQty: 'Used Qty',
planQty: 'Pending Qty',
inputQty: 'Group Qty',
inputQtyPlaceholder: 'Enter qty',
unit: 'Unit',
confirm: 'Confirm',
submitSuccess: 'Submitted successfully',
},
}
const messages = {zh, en}

View File

@@ -55,6 +55,12 @@ const routes = [
component: () => import('@/views/FlatOutBound.vue'),
meta: { requiresAuth: true },
},
{
path: '/product-group',
name: 'ProductGroup',
component: () => import('@/views/ProductGroup.vue'),
meta: { requiresAuth: true },
},
{
path: '/developing',
name: 'Developing',

View File

@@ -41,9 +41,6 @@ const menuIcons = [
'gift-o',
'bag-o',
'apps-o',
'records',
'todo-list-o',
'records',
]
const menuRoutes = {
@@ -54,7 +51,8 @@ const menuRoutes = {
4: '/inventory',
5: '/bind-unbind',
6: '/flat-outbound',
7: '/flat-outbound',
7: '/developing',
8: '/product-group',
}
function onMenuClick(index) {

View File

@@ -0,0 +1,408 @@
<template>
<div class="page-container">
<van-nav-bar
:title="t('productGroup.title')"
left-arrow
@click-left="router.push('/')"
/>
<div class="page-content">
<van-form ref="formRef">
<van-field
v-model="workSectText"
is-link
readonly
:label="t('productGroup.workSect')"
:placeholder="t('productGroup.workSectPlaceholder')"
:rules="[{ required: true, message: t('productGroup.workSectRequired') }]"
name="workSect"
@click="showWorkSectPicker = true"
/>
<van-field
v-model="palletCode"
:label="t('productGroup.palletCode')"
:placeholder="t('productGroup.palletCodePlaceholder')"
:rules="[{ required: true, message: t('productGroup.palletCodeRequired') }]"
name="palletCode"
clearable
>
<template #right-icon>
<van-icon name="scan" class="scan-icon" @click="onScanPallet" />
</template>
</van-field>
<van-field
v-model="workOrderText"
is-link
readonly
:label="t('productGroup.workOrder')"
:placeholder="t('productGroup.workOrderPlaceholder')"
:rules="[{ required: true, message: t('productGroup.workOrderRequired') }]"
name="workOrder"
@click="onWorkOrderClick"
/>
<van-field
v-model="loadPortText"
is-link
readonly
:label="t('productGroup.loadPort')"
:placeholder="t('productGroup.loadPortPlaceholder')"
:rules="[{ required: true, message: t('productGroup.loadPortRequired') }]"
name="loadPort"
@click="onLoadPortClick"
/>
</van-form>
<div class="detail-section">
<div class="detail-header">
<span class="detail-header-text">{{ t('productGroup.total', bomList.length) }}</span>
</div>
<div v-if="!bomList.length" class="detail-empty">
<van-empty :description="t('productGroup.noData')" image="search" />
</div>
<div v-else class="card-list">
<div
v-for="(item, idx) in bomList"
:key="idx"
class="detail-card"
>
<div class="card-header">
<span class="card-index">#{{ idx + 1 }}</span>
</div>
<div class="card-body">
<div class="card-row">
<span class="card-label">{{ t('productGroup.materialCode') }}</span>
<span class="card-value">{{ item.MaterialCode }}</span>
</div>
<div class="card-row">
<span class="card-label">{{ t('productGroup.materialName') }}</span>
<span class="card-value">{{ item.MaterialName }}</span>
</div>
<div class="card-row">
<span class="card-label">{{ t('productGroup.bomQty') }}</span>
<span class="card-value card-value--num">{{ item.BomQty }}</span>
</div>
<div class="card-row">
<span class="card-label">{{ t('productGroup.useBomQty') }}</span>
<span class="card-value">{{ item.UseBomQty }}</span>
</div>
<div class="card-row">
<span class="card-label">{{ t('productGroup.planQty') }}</span>
<span class="card-value card-value--num">{{ item.defaultQty }}</span>
</div>
<div class="card-row card-row--input">
<span class="card-label">{{ t('productGroup.inputQty') }}</span>
<van-field
v-model="item.inputQty"
type="number"
class="card-input"
:placeholder="t('productGroup.inputQtyPlaceholder')"
/>
</div>
<div class="card-row">
<span class="card-label">{{ t('productGroup.unit') }}</span>
<span class="card-value">{{ item.Unit }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
<van-popup v-model:show="showWorkSectPicker" round position="bottom">
<van-picker
:columns="workSectOptions"
@confirm="onWorkSectConfirm"
@cancel="showWorkSectPicker = false"
/>
</van-popup>
<van-popup v-model:show="showWorkOrderPicker" round position="bottom">
<van-picker
:columns="workOrderOptions"
@confirm="onWorkOrderConfirm"
@cancel="showWorkOrderPicker = false"
/>
</van-popup>
<van-popup v-model:show="showLoadPortPicker" round position="bottom">
<van-picker
:columns="loadPortOptions"
@confirm="onLoadPortConfirm"
@cancel="showLoadPortPicker = false"
/>
</van-popup>
<BottomButton :text="t('productGroup.confirm')" :loading="submitting" @click="onSubmit" />
</div>
</template>
<script setup>
import { ref, computed, watch, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { showToast } from 'vant'
import { useI18n } from '@/i18n'
import BottomButton from '@/components/BottomButton.vue'
import request from '@/utils/request'
const router = useRouter()
const { t } = useI18n()
const workSect = ref('')
const workSectText = ref('')
const palletCode = ref('')
const selectedOrderCode = ref('')
const workOrderText = ref('')
const selectedLoadPort = ref('')
const loadPortText = ref('')
const bomList = ref([])
const formRef = ref()
const submitting = ref(false)
const orderList = ref([])
const currentOrder = ref(null)
const showWorkSectPicker = ref(false)
const showWorkOrderPicker = ref(false)
const showLoadPortPicker = ref(false)
const workSectOptions = [
{ text: 'work1', value: 'work1' },
{ text: 'work2', value: 'work2' },
]
const workOrderOptions = computed(() =>
orderList.value.map((item) => ({
text: item.OrderCode,
value: item.OrderCode,
}))
)
const loadPortOptions = computed(() => {
if (!currentOrder.value || !currentOrder.value.PortList) return []
return currentOrder.value.PortList.map((p) => ({
text: p.Port,
value: p.Port,
}))
})
async function fetchWorkOrders() {
try {
const res = await request.get('/api/workOrder', { params: { status: 2 } })
orderList.value = res.content || []
} catch {
// error handled by interceptor
}
}
function onWorkSectConfirm({ selectedOptions }) {
const opt = selectedOptions[0]
workSect.value = opt.value
workSectText.value = opt.text
showWorkSectPicker.value = false
}
function onWorkOrderClick() {
if (!workOrderOptions.value.length) {
showToast({ message: t('productGroup.noOrder'), type: 'fail' })
return
}
showWorkOrderPicker.value = true
}
function onWorkOrderConfirm({ selectedOptions }) {
const opt = selectedOptions[0]
selectedOrderCode.value = opt.value
workOrderText.value = opt.text
currentOrder.value = orderList.value.find((o) => o.OrderCode === opt.value) || null
selectedLoadPort.value = ''
loadPortText.value = ''
bomList.value = []
showWorkOrderPicker.value = false
}
function onLoadPortClick() {
if (!currentOrder.value || !loadPortOptions.value.length) {
showToast({ message: t('productGroup.noPort'), type: 'fail' })
return
}
showLoadPortPicker.value = true
}
function onLoadPortConfirm({ selectedOptions }) {
const opt = selectedOptions[0]
selectedLoadPort.value = opt.value
loadPortText.value = opt.text
filterBomList()
showLoadPortPicker.value = false
}
function filterBomList() {
if (!currentOrder.value || !selectedLoadPort.value) {
bomList.value = []
return
}
const allBom = currentOrder.value.WorkOrderBomList || []
bomList.value = allBom
.filter((item) => item.LoadPort === selectedLoadPort.value)
.map((item) => ({
...item,
defaultQty: item.BomQty - item.UseBomQty,
inputQty: String(item.BomQty - item.UseBomQty),
}))
}
function onScanPallet() {
palletCode.value = 'PLT' + Date.now().toString().slice(-6)
}
async function onSubmit() {
try {
await formRef.value?.validate()
} catch {
return
}
if (!bomList.value.length) {
showToast({ message: t('productGroup.noData'), type: 'fail' })
return
}
const bomDataList = bomList.value.map((item) => ({
id: item.Id,
materialCode: item.MaterialCode,
unit: item.Unit,
useBomQty: Number(item.inputQty) || 0,
}))
submitting.value = true
try {
const res = await request.post('/api/productGroup', {
workSect: workSect.value,
workOrder: selectedOrderCode.value,
palletCode: palletCode.value,
loadPort: selectedLoadPort.value,
bomDataList,
})
showToast({
message: t('productGroup.submitSuccess'),
type: 'success',
})
workSect.value = ''
workSectText.value = ''
palletCode.value = ''
selectedOrderCode.value = ''
workOrderText.value = ''
selectedLoadPort.value = ''
loadPortText.value = ''
currentOrder.value = null
bomList.value = []
} catch {
// error handled by interceptor
} finally {
submitting.value = false
}
}
onMounted(() => {
fetchWorkOrders()
})
</script>
<style scoped>
.detail-section {
margin-top: 12px;
}
.detail-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 0;
}
.detail-header-text {
font-size: 14px;
color: #666;
font-weight: 600;
}
.detail-empty {
padding: 20px 0;
}
.card-list {
display: flex;
flex-direction: column;
gap: 10px;
padding-bottom: 10px;
}
.detail-card {
background: #fff;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
}
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
background: var(--primary-gradient);
color: #fff;
}
.card-index {
font-size: 14px;
font-weight: 600;
}
.card-body {
padding: 10px 12px;
}
.card-row {
display: flex;
align-items: flex-start;
padding: 4px 0;
font-size: 14px;
line-height: 1.5;
}
.card-row--input {
align-items: center;
}
.card-label {
flex-shrink: 0;
width: 80px;
color: #999;
}
.card-value {
flex: 1;
color: #333;
word-break: break-all;
}
.card-value--num {
color: var(--primary-color);
font-weight: 600;
}
.card-input {
flex: 1;
padding: 0;
background: transparent;
}
.card-input :deep(.van-field__control) {
font-size: 14px;
color: var(--primary-color);
font-weight: 600;
}
.card-input :deep(.van-field__body) {
padding: 2px 8px;
border: 1px solid var(--border-color);
border-radius: 4px;
background: #fff;
}
</style>