add:pda出库确认页面

This commit is contained in:
zhangzq
2026-06-13 13:51:29 +08:00
parent 20aebd4888
commit e28627d001
17 changed files with 1474 additions and 1721 deletions

2630
pda/pda/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -25,12 +25,11 @@ const zh = {
'平库上架',
'平库库存查询',
'库位绑定/解绑',
'拣选大厅',
'无容器收货入库',
'空托上架/下架/注册/呼叫',
'AGV配送',
'人工盘点',
'备货组盘查询',
'平库出库'
// '空托上架/下架/注册/呼叫',
// 'AGV配送',
// '人工盘点',
// '备货组盘查询',
],
},
receiveGroup: {
@@ -131,6 +130,23 @@ const zh = {
submit: '提交',
submitSuccess: '提交成功',
},
flatOutBound: {
title: '平库出库',
sectCode: '库区',
sectCodePlaceholder: '请选择库区',
sectCodeRequired: '请选择库区',
selectSect: '选择库区',
total: '共{0}条',
loading: '加载中...',
noData: '暂无待出库物料',
materialCode: '物料编码',
structCode: '库位号',
vehicleCode: '载具号',
planQty: '计划数量',
assignQty: '分配数量',
complete: '完成',
completeSuccess: '操作成功',
},
}
const en = {
@@ -265,6 +281,23 @@ const en = {
submit: 'Submit',
submitSuccess: 'Submitted successfully',
},
flatOutBound: {
title: 'Flat Outbound',
sectCode: 'Warehouse Area',
sectCodePlaceholder: 'Please select warehouse area',
sectCodeRequired: 'Please select warehouse area',
selectSect: 'Select Warehouse Area',
total: 'Total {0} items',
loading: 'Loading...',
noData: 'No pending outbound materials',
materialCode: 'Material Code',
structCode: 'Location',
vehicleCode: 'Vehicle Code',
planQty: 'Plan Qty',
assignQty: 'Assign Qty',
complete: 'Complete',
completeSuccess: 'Success',
},
}
const messages = {zh, en}

View File

@@ -49,6 +49,12 @@ const routes = [
component: () => import('@/views/ManualInbound.vue'),
meta: { requiresAuth: true },
},
{
path: '/flat-outbound',
name: 'FlatOutBound',
component: () => import('@/views/FlatOutBound.vue'),
meta: { requiresAuth: true },
},
{
path: '/developing',
name: 'Developing',

View File

@@ -0,0 +1,315 @@
<template>
<div class="page-container">
<van-nav-bar
:title="t('flatOutBound.title')"
left-arrow
@click-left="router.push('/')"
/>
<div class="page-content">
<van-form ref="formRef">
<van-field
v-model="sectName"
:label="t('flatOutBound.sectCode')"
:placeholder="t('flatOutBound.sectCodePlaceholder')"
:rules="[{ required: true, message: t('flatOutBound.sectCodeRequired') }]"
name="sectCode"
readonly
is-link
@click="showSectPicker = true"
/>
</van-form>
<div class="detail-section">
<div class="detail-header">
<span class="detail-header-text">{{ t('flatOutBound.total', materialList.length) }}</span>
</div>
<div v-if="loading" class="detail-loading">
<van-loading size="24px">{{ t('flatOutBound.loading') }}</van-loading>
</div>
<div v-else-if="!materialList.length" class="detail-empty">
<van-empty :description="t('flatOutBound.noData')" image="search" />
</div>
<div v-else class="card-list">
<div
v-for="(item, idx) in materialList"
:key="idx"
class="detail-card"
>
<div class="card-header">
<span class="card-index">#{{ idx + 1 }}</span>
</div>
<div class="card-body">
<div class="card-content">
<div class="card-left">
<div class="card-row">
<div class="field-group">
<span class="field-label">库区</span>
<span class="field-value">{{ item.sectName }}</span>
</div>
<div class="field-group">
<span class="field-label">库位号</span>
<span class="field-value">{{ item.structCode }}</span>
</div>
</div>
<div class="card-row">
<div class="field-group">
<span class="field-label">载具号</span>
<span class="field-value">{{ item.storagevehicleCode }}</span>
</div>
<div class="field-group">
<span class="field-label">物料编码</span>
<span class="field-value">{{ item.materialCode }}</span>
</div>
</div>
<div class="card-row">
<div class="field-group">
<span class="field-label">计划数量</span>
<span class="field-value field-value--primary">{{ item.planQty }}</span>
</div>
<div class="field-group field-group--stepper">
<span class="field-label">分配数量</span>
<van-stepper
v-model="item.assignQty"
:min="0"
:step="1"
:decimal-length="3"
input-width="60px"
button-size="28px"
/>
</div>
</div>
</div>
<div class="card-right">
<van-button
type="primary"
size="normal"
:loading="item.submitting"
@click="onComplete(item, idx)"
>
完成
</van-button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<van-popup v-model:show="showSectPicker" position="bottom">
<van-picker
:columns="sectOptions"
:title="t('flatOutBound.selectSect')"
@confirm="onSectConfirm"
@cancel="showSectPicker = false"
/>
</van-popup>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { showToast } from 'vant'
import { useI18n } from '@/i18n'
import request from '@/utils/request'
const router = useRouter()
const { t } = useI18n()
const sectName = ref('')
const sectCode = ref('')
const showSectPicker = ref(false)
const sectOptions = ref([])
const materialList = ref([])
const loading = ref(false)
async function fetchFlatWarehouse() {
try {
const res = await request.get('/api/pda/iosOut/flatWarehouse')
if (res.data && Array.isArray(res.data)) {
sectOptions.value = res.data.map(item => ({
text: item.label,
value: item.value,
}))
}
} catch {
// error handled by interceptor
}
}
async function fetchOutBoundDtl(code) {
if (!code) return
loading.value = true
try {
const res = await request.get('/api/pda/iosOut/getOutBoundDtl', {
params: { sectCode: code },
})
if (res.data && Array.isArray(res.data)) {
materialList.value = res.data.map(item => ({
...item,
assignQty: item.assignQty ?? item.planQty,
submitting: false,
}))
} else {
materialList.value = []
}
} catch {
materialList.value = []
} finally {
loading.value = false
}
}
function onSectConfirm({ selectedOptions }) {
const selected = selectedOptions[0]
sectName.value = selected.text
sectCode.value = selected.value
showSectPicker.value = false
fetchOutBoundDtl(selected.value)
}
async function onComplete(item, idx) {
if (item.submitting) return
item.submitting = true
try {
const res = await request.post('/api/pda/iosOut/assignOutDis', {
item
})
showToast({ message: res.message || t('flatOutBound.completeSuccess'), type: 'success' })
materialList.value.splice(idx, 1)
} catch {
// error handled by interceptor
} finally {
item.submitting = false
}
}
onMounted(() => {
fetchFlatWarehouse()
})
</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-loading {
display: flex;
justify-content: center;
padding: 40px 0;
}
.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: 12px 16px;
}
.card-content {
display: flex;
align-items: center;
gap: 0;
}
.card-left {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 10px;
}
.card-row {
display: flex;
align-items: center;
gap: 0;
}
.field-group {
flex: 1;
display: flex;
align-items: center;
gap: 12px;
min-width: 0;
}
.field-group--stepper {
align-items: center;
}
.field-label {
flex-shrink: 0;
width: 56px;
text-align: right;
color: #999;
font-size: 13px;
}
.field-value {
flex: 1;
color: #333;
font-size: 14px;
word-break: break-all;
}
.field-value--primary {
color: var(--primary-color);
font-weight: 600;
}
.card-right {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
padding-left: 16px;
margin-left: 16px;
border-left: 1px solid #f0f0f0;
align-self: stretch;
}
</style>

View File

@@ -53,6 +53,8 @@ const menuRoutes = {
3: '/putaway',
4: '/inventory',
5: '/bind-unbind',
6: '/flat-outbound',
7: '/flat-outbound',
}
function onMenuClick(index) {

View File

@@ -5,8 +5,8 @@
</div>
<div class="login-header">
<div class="login-logo">
<div class="logo-box">ZD</div>
<div class="logo-text">{{ t('login.title') }}</div>
<div class="logo-box">诺力</div>
<div class="logo-text">WMS系统</div>
</div>
</div>
<div class="login-form">