add:生产下料同步

This commit is contained in:
zhangzq
2026-06-17 13:22:57 +08:00
parent 64174b4109
commit b73e8ff174
2 changed files with 410 additions and 0 deletions

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>

View File

@@ -34,3 +34,5 @@ CREATE TABLE IF NOT EXISTS `st_ivt_purchasedtl` (
`instock_qty` DECIMAL(18,6) DEFAULT 0 COMMENT '已入库数量'
)COMMENT='采购入库单分录信息表';
ALTER TABLE `wms_nlwq`.`st_ivt_purchasemst`
ADD COLUMN `forwardZD` tinyint(1) NULL DEFAULT 0 COMMENT '0否 1是' AFTER `audit_msg`;