add:生产下料同步
This commit is contained in:
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>
|
||||||
@@ -34,3 +34,5 @@ CREATE TABLE IF NOT EXISTS `st_ivt_purchasedtl` (
|
|||||||
`instock_qty` DECIMAL(18,6) DEFAULT 0 COMMENT '已入库数量'
|
`instock_qty` DECIMAL(18,6) DEFAULT 0 COMMENT '已入库数量'
|
||||||
)COMMENT='采购入库单分录信息表';
|
)COMMENT='采购入库单分录信息表';
|
||||||
|
|
||||||
|
ALTER TABLE `wms_nlwq`.`st_ivt_purchasemst`
|
||||||
|
ADD COLUMN `forwardZD` tinyint(1) NULL DEFAULT 0 COMMENT '0否 1是' AFTER `audit_msg`;
|
||||||
Reference in New Issue
Block a user