add:PDA配盘入库功能
This commit is contained in:
@@ -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},
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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) {
|
||||
|
||||
408
pda/pda/src/views/ProductGroup.vue
Normal file
408
pda/pda/src/views/ProductGroup.vue
Normal 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>
|
||||
Reference in New Issue
Block a user