front/src/views/warehouse/inboundNotice/index.vue
xiaolipro a0dfdffa14
Some checks failed
Build and Deploy Front / deploy (push) Failing after 2m17s
fix:编码乱码
2026-09-07 14:36:45 +08:00

896 lines
32 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup lang="ts">
import { computed, onMounted, reactive, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { Modal } from '@arco-design/web-vue'
import HandsontablePro from '@/components/HandsontablePro/index.vue'
import PaginationPro from '@/components/PaginationPro/index.vue'
import ModalPro from '@/components/ModalPro/index.vue'
import useModal from '@/components/ModalPro/useModal'
import { useTableContainerHeight } from '@/composables/useTableContainerHeight'
import {
confirmWarehouseInboundReceive,
deleteWarehouseInbound,
getWarehouseBinLocationList,
getWarehouseInboundDetail,
getWarehouseInboundList,
getWarehouseInboundSummary,
getWarehouseSkuList,
saveWarehouseInbound,
} from '@/api/warehouse'
import { getWarehouseList } from '@/api/system/warehouse'
import { $baseMessage } from '@/utils'
import { hasPermission } from '@/utils/rbac-permission'
import {
buildWarehouseNameMap,
formatDateTime,
normalizeListRow,
pickField,
resolveWarehouseLabel,
} from '@/utils/warehouseListMapper'
import { renderWarehouseActionBtn, wrapWarehouseActions } from '@/utils/warehouseActionRenderer'
import { buildStatusTagHtml, type StatusTagTone } from '@/utils/statusTag'
import { useWarehouseAdminCustomer } from '@/composables/useWarehouseAdminCustomer'
import { useCurrentWarehouseStore } from '@/store'
defineOptions({ name: 'WarehouseInboundNotice' })
interface InboundLineForm {
skuId?: number
skuCode: string
plannedQty: number
}
interface InboundBoxForm {
key: string
boxSeqNo: string
markBoxNo: string
weightG?: number
lengthCm?: number
widthCm?: number
heightCm?: number
lines: InboundLineForm[]
}
interface ReceiveLineForm {
lineId: number
skuCode: string
plannedQty: number
receivedQty: number
binLocationId?: number
}
interface ReceiveBoxForm {
boxId: number
markBoxNo: string
boxSeqNo: string
lines: ReceiveLineForm[]
}
const { t } = useI18n()
const { tableContainerRef, tableHeight } = useTableContainerHeight()
const {
isAdmin,
customerId: adminCustomerId,
customerOptions,
withCustomerId,
ensureMutationCustomerId,
resolveSelectedCustomerId,
} = useWarehouseAdminCustomer()
const currentWarehouseStore = useCurrentWarehouseStore()
/** Tab active-key;全部用 'all',请求时再转成后端 receiptStatus */
const STATUS_TAB_ALL = 'all'
const receiptStatus = ref<string>(STATUS_TAB_ALL)
const summary = ref({ forecasted: 0, received: 0, completed: 0 })
const keyword = ref('')
const warehouseId = ref<number>()
const warehouseOptions = ref<Array<{ label: string; value: number }>>([])
const warehouseNameMap = computed(() => buildWarehouseNameMap(warehouseOptions.value))
const skuOptions = ref<Array<{ label: string; value: number; code: string }>>([])
const listLoading = ref(false)
const tableRows = ref<any[]>([])
const currentPage = ref(1)
const pageSize = ref(50)
const totalCount = ref(0)
const selectedBoxKeys = ref<string[]>([])
const activeBoxKey = ref('')
const statusTabs = [
{ value: STATUS_TAB_ALL, labelKey: 'warehouse.inbound.status.all', countKey: null },
{ value: 'forecasted', labelKey: 'warehouse.inbound.status.forecasted', countKey: 'forecasted' as const },
{ value: 'received', labelKey: 'warehouse.inbound.status.received', countKey: 'received' as const },
{ value: 'completed', labelKey: 'warehouse.inbound.status.completed', countKey: 'completed' as const },
]
const slidingStatusTabs = computed(() =>
statusTabs.map((tab) => ({
key: tab.value,
label: tab.countKey
? `${t(tab.labelKey)} (${summary.value[tab.countKey]})`
: t(tab.labelKey),
})),
)
const detailVisible = ref(false)
const detailData = ref<any>(null)
const [registerFormModal, { openModal, closeModal }] = useModal() as any
const [registerReceiveModal, { openModal: openReceiveModal, closeModal: closeReceiveModal }] = useModal() as any
const saving = ref(false)
const receiveSaving = ref(false)
const receiveNotice = ref<any>(null)
const receiveBoxes = ref<ReceiveBoxForm[]>([])
const receiveBinOptions = ref<Array<{ label: string; value: number }>>([])
const formModel = reactive({
id: 0,
warehouseId: undefined as number | undefined,
markReference: '',
inboundType: 'normal',
remark: '',
})
const boxRows = ref<InboundBoxForm[]>([])
const boxTabItems = computed(() =>
boxRows.value.map((box) => ({
key: box.key,
label: `${t('warehouse.inbound.box')} ${box.boxSeqNo}`,
})),
)
const modalWarehouseName = computed(() => {
const wh = warehouseOptions.value.find((w) => w.value === formModel.warehouseId)
return wh?.label || ''
})
const receiveWarehouseName = computed(() => {
const whId = receiveNotice.value?.warehouseId
return resolveWarehouseLabel(whId, warehouseNameMap.value)
})
const boxCount = computed(() => boxRows.value.length)
const totalSkuCount = computed(() =>
boxRows.value.reduce((sum, box) => sum + box.lines.filter((l) => l.skuId).length, 0),
)
const totalPlannedQty = computed(() =>
boxRows.value.reduce(
(sum, box) => sum + box.lines.reduce((s, line) => s + (Number(line.plannedQty) || 0), 0),
0,
),
)
function renderReceiptStatusCell(row: any) {
const code = String(row?._status || '')
const toneMap: Record<string, StatusTagTone> = {
forecasted: 'brand',
received: 'wait',
completed: 'ok',
}
return buildStatusTagHtml(String(row?.receiptStatus || '--'), toneMap[code] || 'neutral')
}
const hotColumns = computed(() => {
const cols = [
{ key: 'noticeNo', label: t('warehouse.inbound.table.noticeNo'), width: 180 },
{ key: 'markReference', label: t('warehouse.inbound.table.markReference'), width: 120 },
{ key: 'warehouseName', label: t('warehouse.inbound.table.warehouse'), width: 140 },
{ key: 'inboundType', label: t('warehouse.inbound.table.inboundType'), width: 100 },
{
key: 'receiptStatus',
label: t('warehouse.inbound.table.receiptStatus'),
width: 100,
renderer: renderReceiptStatusCell,
},
{ key: 'boxCount', label: t('warehouse.inbound.table.boxCount'), width: 80 },
{ key: 'plannedQty', label: t('warehouse.inbound.table.plannedQty'), width: 90 },
{ key: 'expectedArrivalTime', label: t('warehouse.inbound.table.expectedArrival'), width: 160 },
{ key: 'createTime', label: t('warehouse.inbound.table.createTime'), width: 160 },
]
if (isAdmin.value) {
cols.unshift(
{ key: 'customerName', label: t('warehouse.common.customerName'), width: 120 },
{ key: 'customerCode', label: t('warehouse.common.customerCode'), width: 110 },
)
}
return cols
})
function createBox(seq: number): InboundBoxForm {
return {
key: `box-${Date.now()}-${seq}`,
boxSeqNo: String(seq),
markBoxNo: `BOX-${String(seq).padStart(3, '0')}`,
lines: [{ skuId: undefined, skuCode: '', plannedQty: 1 }],
}
}
function resetBoxes() {
const first = createBox(1)
boxRows.value = [first]
selectedBoxKeys.value = []
activeBoxKey.value = first.key
}
function addBox() {
const next = boxRows.value.length + 1
const box = createBox(next)
boxRows.value = [...boxRows.value, box]
activeBoxKey.value = box.key
}
function addBoxes(count: number) {
const start = boxRows.value.length
const added = Array.from({ length: count }, (_, i) => createBox(start + i + 1))
boxRows.value = [...boxRows.value, ...added]
if (added.length) activeBoxKey.value = added[added.length - 1].key
}
function removeActiveBox() {
if (boxRows.value.length <= 1) return
const key = activeBoxKey.value
const remain = boxRows.value.filter((b) => b.key !== key)
boxRows.value = remain
selectedBoxKeys.value = selectedBoxKeys.value.filter((k) => k !== key)
activeBoxKey.value = remain[remain.length - 1]?.key || ''
}
function removeSelectedBoxes() {
if (!selectedBoxKeys.value.length) {
removeActiveBox()
return
}
const remain = boxRows.value.filter((b) => !selectedBoxKeys.value.includes(b.key))
boxRows.value = remain.length ? remain : [createBox(1)]
selectedBoxKeys.value = []
activeBoxKey.value = boxRows.value[boxRows.value.length - 1].key
}
function addLine(box: InboundBoxForm) {
box.lines.push({ skuId: undefined, skuCode: '', plannedQty: 1 })
}
function removeLine(box: InboundBoxForm, index: number) {
if (box.lines.length <= 1) return
box.lines.splice(index, 1)
}
function onSkuChange(line: InboundLineForm, skuId?: number) {
const sku = skuOptions.value.find((s) => s.value === skuId)
line.skuId = skuId
line.skuCode = sku?.code || ''
}
function mapRow(raw: Record<string, unknown>) {
const row = normalizeListRow(raw)
const status = String(pickField(row, 'receiptStatus', 'ReceiptStatus') ?? '')
const inboundType = String(pickField(row, 'inboundType', 'InboundType') ?? 'normal')
const whId = pickField<number>(row, 'warehouseId', 'WarehouseId')
return {
id: pickField<number>(row, 'id', 'Id'),
customerId: pickField<number>(row, 'customerId', 'CustomerId'),
customerName: pickField(row, 'customerName', 'CustomerName') ?? '',
customerCode: pickField(row, 'customerCode', 'CustomerCode') ?? '',
noticeNo: pickField(row, 'noticeNo', 'NoticeNo') ?? '-',
markReference: pickField(row, 'markReference', 'MarkReference') ?? '-',
warehouseName: resolveWarehouseLabel(whId, warehouseNameMap.value),
inboundType: t(`warehouse.inbound.type.${inboundType}`, inboundType),
receiptStatus: t(`warehouse.inbound.status.${status}`, status),
boxCount: pickField<number>(row, 'boxCount', 'BoxCount') ?? 0,
plannedQty: pickField<number>(row, 'plannedQty', 'PlannedQty') ?? 0,
expectedArrivalTime: formatDateTime(pickField(row, 'expectedArrivalTime', 'ExpectedArrivalTime')),
createTime: formatDateTime(pickField(row, 'createTime', 'CreateTime')),
_status: status,
_raw: row,
_status: status,
}
}
async function loadWarehouses() {
const { items } = await getWarehouseList({ page: 1, pageSize: 200 }).catch(() => ({ items: [] }))
warehouseOptions.value = (items || []).map((w: any) => ({
label: w.warehouseName || `#${w.id}`,
value: Number(w.id),
}))
}
async function loadSkus() {
const { items } = await getWarehouseSkuList(withCustomerId({ page: 1, pageSize: 500 })).catch(() => ({ items: [] }))
skuOptions.value = (items || []).map((s: any) => ({
label: `${s.skuCode || s.SkuCode} - ${s.skuName || s.SkuName}`,
value: Number(s.id || s.Id),
code: s.skuCode || s.SkuCode,
}))
}
async function loadSummary() {
try {
const data = await getWarehouseInboundSummary(withCustomerId({
warehouseId: warehouseId.value,
}))
summary.value = {
forecasted: data?.forecasted ?? data?.Forecasted ?? 0,
received: data?.received ?? data?.Received ?? 0,
completed: data?.completed ?? data?.Completed ?? 0,
}
} catch {
summary.value = { forecasted: 0, received: 0, completed: 0 }
}
}
function resolveReceiptStatusFilter(): string | undefined {
const status = receiptStatus.value
if (!status || status === STATUS_TAB_ALL) return undefined
return status
}
async function loadList() {
listLoading.value = true
try {
const { items, total } = await getWarehouseInboundList(withCustomerId({
page: currentPage.value,
pageSize: pageSize.value,
receiptStatus: resolveReceiptStatusFilter(),
keyword: keyword.value || undefined,
warehouseId: warehouseId.value,
}))
tableRows.value = (items || []).map((item: Record<string, unknown>) => mapRow(item))
totalCount.value = Number(total || 0)
} catch (e: any) {
$baseMessage(e?.message || t('warehouse.inbound.message.loadFailed'), 'error')
} finally {
listLoading.value = false
}
}
function handleStatusTabChange(key: string | number) {
receiptStatus.value = String(key)
currentPage.value = 1
loadList()
}
function handlePageChange(page: number) {
currentPage.value = page
loadList()
}
function handlePageSizeChange(size: number) {
pageSize.value = size
currentPage.value = 1
loadList()
}
function handleSearch() {
currentPage.value = 1
loadList()
}
async function showDetail(row: any) {
try {
detailData.value = await getWarehouseInboundDetail(
row._raw?.id || row.id,
isAdmin.value ? resolveSelectedCustomerId() : undefined,
)
detailVisible.value = true
} catch (e: any) {
$baseMessage(e?.message || t('warehouse.inbound.message.detailFailed'), 'error')
}
}
async function loadReceiveBinOptions(whId?: number) {
if (!whId) {
receiveBinOptions.value = []
return
}
try {
const { items } = await getWarehouseBinLocationList({ warehouseId: whId, isEnabled: true })
receiveBinOptions.value = (items || []).map((b: any) => ({
label: `${b.binCode || b.BinCode}${b.binName || b.BinName ? ` - ${b.binName || b.BinName}` : ''}`,
value: Number(b.id || b.Id),
}))
} catch {
receiveBinOptions.value = []
}
}
async function openConfirmReceive(row: any) {
if (
!(
hasPermission('Warehouse.InboundNotice.ConfirmReceive')
|| hasPermission('Warehouse.InboundNotice.Save')
)
|| row._status === 'completed'
) {
return
}
if (isAdmin.value && ensureMutationCustomerId() == null) return
try {
const detail = await getWarehouseInboundDetail(
row._raw?.id || row.id,
isAdmin.value ? resolveSelectedCustomerId() : undefined,
)
const normalized = normalizeListRow(detail || {})
const whId = pickField<number>(normalized, 'warehouseId', 'WarehouseId')
const boxesRaw = pickField(normalized, 'boxes', 'Boxes')
const boxes = Array.isArray(boxesRaw) ? boxesRaw : []
receiveNotice.value = {
id: pickField<number>(normalized, 'id', 'Id'),
noticeNo: pickField(normalized, 'noticeNo', 'NoticeNo') ?? row.noticeNo,
warehouseId: whId,
}
receiveBoxes.value = boxes.map((box: any) => {
const b = normalizeListRow(box)
const linesRaw = pickField(b, 'lines', 'Lines')
const lines = Array.isArray(linesRaw) ? linesRaw : []
return {
boxId: Number(pickField(b, 'id', 'Id') || 0),
markBoxNo: String(pickField(b, 'markBoxNo', 'MarkBoxNo') ?? ''),
boxSeqNo: String(pickField(b, 'boxSeqNo', 'BoxSeqNo') ?? ''),
lines: lines.map((line: any) => {
const l = normalizeListRow(line)
const planned = Number(pickField(l, 'plannedQty', 'PlannedQty') ?? 0)
const received = pickField<number>(l, 'receivedQty', 'ReceivedQty')
return {
lineId: Number(pickField(l, 'id', 'Id') || 0),
skuCode: String(pickField(l, 'skuCode', 'SkuCode') ?? '-'),
plannedQty: planned,
receivedQty: received != null ? Number(received) : planned,
binLocationId: pickField<number>(l, 'binLocationId', 'BinLocationId') || undefined,
}
}),
}
})
await loadReceiveBinOptions(whId)
openReceiveModal(true)
} catch (e: any) {
$baseMessage(e?.message || t('warehouse.inbound.message.detailFailed'), 'error')
}
}
async function submitConfirmReceive() {
const notice = receiveNotice.value
if (!notice?.id) return
if (isAdmin.value && ensureMutationCustomerId() == null) return
for (const box of receiveBoxes.value) {
for (const line of box.lines) {
if (line.receivedQty > 0 && !line.binLocationId) {
$baseMessage(t('warehouse.inbound.message.receiveBinRequired'), 'warning')
return
}
if (line.receivedQty < 0) {
$baseMessage(t('warehouse.inbound.message.receiveQtyInvalid'), 'warning')
return
}
}
}
receiveSaving.value = true
try {
await confirmWarehouseInboundReceive(withCustomerId({
inboundNoticeId: notice.id,
boxes: receiveBoxes.value.map((box) => ({
boxId: box.boxId,
lines: box.lines.map((line) => ({
lineId: line.lineId,
receivedQty: Number(line.receivedQty) || 0,
binLocationId: line.receivedQty > 0 ? Number(line.binLocationId) : 0,
})),
})),
}))
$baseMessage(t('warehouse.inbound.message.receiveSuccess'), 'success')
closeReceiveModal()
await loadSummary()
await loadList()
} catch (e: any) {
$baseMessage(e?.message || t('warehouse.inbound.message.receiveFailed'), 'error')
} finally {
receiveSaving.value = false
}
}
function handleAdd() {
if (!hasPermission('Warehouse.InboundNotice.Save')) return
if (isAdmin.value && ensureMutationCustomerId() == null) return
formModel.id = 0
formModel.warehouseId = currentWarehouseStore.resolveDefaultWarehouseId(warehouseId.value)
formModel.markReference = ''
formModel.remark = ''
formModel.inboundType = 'normal'
resetBoxes()
if (!formModel.warehouseId) {
$baseMessage(t('warehouse.common.noCurrentWarehouse'), 'warning')
return
}
openModal(true)
}
async function handleSave() {
if (!formModel.warehouseId) {
$baseMessage(t('warehouse.inbound.message.required'), 'warning')
return
}
if (isAdmin.value && ensureMutationCustomerId() == null) return
const boxes = boxRows.value
.map((box) => ({
boxSeqNo: box.boxSeqNo,
markBoxNo: box.markBoxNo,
weightG: box.weightG,
lengthCm: box.lengthCm,
widthCm: box.widthCm,
heightCm: box.heightCm,
lines: box.lines
.filter((line) => line.skuId)
.map((line) => ({
skuId: line.skuId!,
skuCode: line.skuCode,
plannedQty: Number(line.plannedQty) || 1,
})),
}))
.filter((box) => box.lines.length > 0)
if (!boxes.length) {
$baseMessage(t('warehouse.inbound.message.boxRequired'), 'warning')
return
}
saving.value = true
try {
await saveWarehouseInbound(withCustomerId({
id: formModel.id,
warehouseId: formModel.warehouseId,
markReference: formModel.markReference,
inboundType: formModel.inboundType,
remark: formModel.remark,
boxes,
}))
$baseMessage(t('warehouse.inbound.message.saveSuccess'), 'success')
closeModal()
await loadSummary()
await loadList()
} catch (e: any) {
$baseMessage(e?.message || t('warehouse.inbound.message.saveFailed'), 'error')
} finally {
saving.value = false
}
}
function handleDelete(row: any) {
if (!hasPermission('Warehouse.InboundNotice.Delete')) return
if (isAdmin.value && ensureMutationCustomerId() == null) return
Modal.confirm({
title: t('warehouse.inbound.actions.delete'),
content: row.noticeNo,
onOk: async () => {
await deleteWarehouseInbound(row._raw?.id || row.id)
$baseMessage(t('warehouse.inbound.message.deleteSuccess'), 'success')
await loadSummary()
await loadList()
},
})
}
function actionRenderer(row: any) {
const parts = [renderWarehouseActionBtn('detail', t('warehouse.common.detail'), 'view')]
if (
(hasPermission('Warehouse.InboundNotice.ConfirmReceive')
|| hasPermission('Warehouse.InboundNotice.Save'))
&& row._status !== 'completed'
) {
parts.push(renderWarehouseActionBtn('receive', t('warehouse.inbound.actions.confirmReceive')))
}
if (hasPermission('Warehouse.InboundNotice.Delete') && row._status === 'forecasted') {
parts.push(renderWarehouseActionBtn('delete', t('warehouse.common.delete')))
}
return wrapWarehouseActions(parts)
}
function handleActionClick(row: any, _i: number, action?: string) {
if (action === 'detail') showDetail(row)
if (action === 'receive') openConfirmReceive(row)
if (action === 'delete') handleDelete(row)
}
onMounted(async () => {
await loadWarehouses()
await loadSkus()
await loadSummary()
await loadList()
})
</script>
<template>
<div class="warehouse-inbound-container warehouse-list-page">
<div class="warehouse-list-page-card">
<div class="status-tabs">
<SlidingTabs
:model-value="receiptStatus"
size="mini"
:items="slidingStatusTabs"
@change="handleStatusTabChange"
/>
</div>
<div class="table-toolbar">
<div class="toolbar-left">
<a-space wrap :size="8">
<a-select
v-if="isAdmin"
v-model="adminCustomerId"
allow-clear
allow-search
:placeholder="t('warehouse.common.selectCustomer')"
:options="customerOptions"
style="width: 220px"
@change="() => { loadSkus(); loadSummary(); handleSearch() }"
/>
<a-select
v-model="warehouseId"
allow-clear
:placeholder="t('warehouse.common.warehouse')"
:options="warehouseOptions"
style="width: 200px"
@change="() => { loadSummary(); handleSearch() }"
/>
<a-input
v-model="keyword"
allow-clear
:placeholder="t('warehouse.inbound.search.keyword')"
style="width: 200px"
@press-enter="handleSearch"
/>
<a-button class="app-btn-search" @click="handleSearch">{{ t('warehouse.common.search') }}</a-button>
<a-button v-if="hasPermission('Warehouse.InboundNotice.Save')" class="btn-add app-btn-primary-soft" @click="handleAdd">
{{ t('warehouse.common.add') }}
</a-button>
</a-space>
</div>
<div class="toolbar-right">
<PaginationPro
:total-count="totalCount"
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[10, 30, 50, 100]"
@page-change="handlePageChange"
@page-size-change="handlePageSizeChange"
/>
</div>
</div>
<div ref="tableContainerRef" class="list-wrap">
<HandsontablePro
:columns="hotColumns"
:data="tableRows"
:height="tableHeight"
:loading="listLoading"
:stretch-columns="true"
show-action-column
:action-renderer="actionRenderer"
@action-click="handleActionClick"
/>
</div>
</div>
<a-drawer v-model:visible="detailVisible" :title="t('warehouse.inbound.drawer.detail')" :width="640">
<template v-if="detailData">
<p>{{ t('warehouse.inbound.table.noticeNo') }}: {{ detailData.noticeNo || detailData.NoticeNo }}</p>
<p>{{ t('warehouse.inbound.table.markReference') }}: {{ detailData.markReference || detailData.MarkReference || '-' }}</p>
<div v-for="box in (detailData.boxes || detailData.Boxes || [])" :key="box.id || box.Id" class="box-block">
<div class="box-title">{{ t('warehouse.inbound.box') }}: {{ box.markBoxNo || box.MarkBoxNo || box.boxSeqNo || box.BoxSeqNo }}</div>
<a-table :data="box.lines || box.Lines || []" :pagination="false" size="small">
<template #columns>
<a-table-column :title="t('warehouse.inbound.line.skuCode')" data-index="skuCode" />
<a-table-column :title="t('warehouse.inbound.line.plannedQty')" data-index="plannedQty" />
<a-table-column :title="t('warehouse.inbound.line.receivedQty')" data-index="receivedQty" />
</template>
</a-table>
</div>
</template>
</a-drawer>
<ModalPro
:title="t('warehouse.inbound.modal.confirmReceive')"
:width="860"
:mask-closable="false"
:body-style="{ maxHeight: '70vh', overflow: 'auto' }"
@register="registerReceiveModal"
@cancel="closeReceiveModal"
>
<div class="wh-form-modal">
<div v-if="receiveNotice" class="wh-form-info-bar">
<div>{{ t('warehouse.inbound.table.noticeNo') }}:{{ receiveNotice.noticeNo }}</div>
<div>{{ t('warehouse.common.warehouse') }}:{{ receiveWarehouseName || '-' }}</div>
</div>
<div v-for="box in receiveBoxes" :key="box.boxId" class="receive-box-block">
<div class="box-title">
{{ t('warehouse.inbound.box') }} {{ box.boxSeqNo }}
<span v-if="box.markBoxNo">({{ box.markBoxNo }})</span>
</div>
<a-table :data="box.lines" :pagination="false" size="small" :bordered="{ cell: true }">
<template #columns>
<a-table-column :title="t('warehouse.inbound.line.skuCode')" data-index="skuCode" :width="140" />
<a-table-column :title="t('warehouse.inbound.line.plannedQty')" data-index="plannedQty" :width="90" />
<a-table-column :title="t('warehouse.inbound.line.receivedQty')" :width="130">
<template #cell="{ record }">
<a-input-number v-model="record.receivedQty" :min="0" style="width: 100%" />
</template>
</a-table-column>
<a-table-column :title="t('warehouse.inbound.line.binLocation')" :width="220">
<template #cell="{ record }">
<a-select
v-model="record.binLocationId"
:options="receiveBinOptions"
allow-search
allow-clear
:placeholder="t('warehouse.inbound.form.selectBin')"
:disabled="!(record.receivedQty > 0)"
/>
</template>
</a-table-column>
</template>
</a-table>
</div>
</div>
<template #footer>
<a-space :size="8">
<a-button @click="closeReceiveModal">{{ t('warehouse.common.cancel') }}</a-button>
<a-button type="primary" :loading="receiveSaving" @click="submitConfirmReceive">
{{ t('warehouse.inbound.actions.confirmReceive') }}
</a-button>
</a-space>
</template>
</ModalPro>
<ModalPro
:title="t('warehouse.inbound.modal.add')"
:width="800"
:mask-closable="false"
:body-style="{ maxHeight: '70vh', overflow: 'auto' }"
@register="registerFormModal"
@cancel="closeModal"
>
<div class="wh-form-modal">
<a-form
:model="formModel"
layout="horizontal"
:label-col-props="{ span: 6 }"
:wrapper-col-props="{ span: 16 }"
>
<a-form-item :label="t('warehouse.parcel.form.currentWarehouse')" required>
<span class="wh-form-readonly">
{{
modalWarehouseName
|| warehouseOptions.find((o) => o.value === formModel.warehouseId)?.label
|| currentWarehouseStore.warehouseName
|| formModel.warehouseId
|| '-'
}}
</span>
</a-form-item>
<a-form-item :label="t('warehouse.inbound.form.markReference')">
<a-input v-model="formModel.markReference" :placeholder="t('warehouse.inbound.form.markPlaceholder')" allow-clear />
</a-form-item>
<a-form-item :label="t('warehouse.inbound.table.inboundType')">
<a-select v-model="formModel.inboundType">
<a-option value="normal">{{ t('warehouse.inbound.type.normal') }}</a-option>
<a-option value="return">{{ t('warehouse.inbound.type.return') }}</a-option>
</a-select>
</a-form-item>
<a-form-item :label="t('warehouse.parcel.form.remark')">
<a-input v-model="formModel.remark" allow-clear />
</a-form-item>
</a-form>
<a-divider class="wh-form-divider" />
<div class="wh-form-section__head">
<div class="wh-form-section__title">{{ t('warehouse.inbound.form.boxSkuDetail') }}</div>
<div class="wh-form-meta">
<a-tag color="arcoblue">{{ t('warehouse.inbound.form.boxCount', { n: boxCount }) }}</a-tag>
<a-tag color="green">{{ t('warehouse.inbound.form.skuCount', { n: totalSkuCount }) }}</a-tag>
<a-tag color="purple">{{ t('warehouse.inbound.form.totalQty', { n: totalPlannedQty }) }}</a-tag>
</div>
</div>
<div class="wh-form-toolbar">
<a-button type="primary" size="small" @click="addBox">{{ t('warehouse.inbound.actions.addBox') }}</a-button>
<a-button size="small" @click="addBoxes(5)">{{ t('warehouse.inbound.actions.addBox5') }}</a-button>
<a-button size="small" status="danger" @click="removeSelectedBoxes">{{ t('warehouse.inbound.actions.removeBox') }}</a-button>
</div>
<div class="wh-form-box-tabs">
<SlidingTabs v-model="activeBoxKey" size="mini" :items="boxTabItems" />
</div>
<div
v-for="box in boxRows"
v-show="activeBoxKey === box.key"
:key="box.key"
class="wh-form-box-panel"
>
<a-row :gutter="12">
<a-col :span="8">
<a-form-item :label="t('warehouse.inbound.form.markBoxNo')" :label-col-props="{ span: 24 }">
<a-input v-model="box.markBoxNo" allow-clear />
</a-form-item>
</a-col>
<a-col :span="4">
<a-form-item :label="t('warehouse.inbound.form.boxWeight')" :label-col-props="{ span: 24 }">
<a-input-number v-model="box.weightG" :min="0" style="width: 100%" />
</a-form-item>
</a-col>
<a-col :span="4">
<a-form-item :label="t('warehouse.inbound.form.boxLength')" :label-col-props="{ span: 24 }">
<a-input-number v-model="box.lengthCm" :min="0" style="width: 100%" />
</a-form-item>
</a-col>
<a-col :span="4">
<a-form-item :label="t('warehouse.inbound.form.boxWidth')" :label-col-props="{ span: 24 }">
<a-input-number v-model="box.widthCm" :min="0" style="width: 100%" />
</a-form-item>
</a-col>
<a-col :span="4">
<a-form-item :label="t('warehouse.inbound.form.boxHeight')" :label-col-props="{ span: 24 }">
<a-input-number v-model="box.heightCm" :min="0" style="width: 100%" />
</a-form-item>
</a-col>
</a-row>
<div class="wh-form-toolbar">
<a-button type="primary" size="small" @click="addLine(box)">{{ t('warehouse.inbound.actions.addSku') }}</a-button>
</div>
<a-table :data="box.lines" :pagination="false" size="small" :bordered="{ cell: true }">
<template #columns>
<a-table-column :title="t('warehouse.inbound.form.sku')">
<template #cell="{ record }">
<a-select
v-model="record.skuId"
:options="skuOptions"
allow-search
:placeholder="t('warehouse.inbound.form.selectSku')"
@change="(v: number) => onSkuChange(record, v)"
/>
</template>
</a-table-column>
<a-table-column :title="t('warehouse.inbound.line.plannedQty')" :width="130">
<template #cell="{ record }">
<a-input-number v-model="record.plannedQty" :min="1" style="width: 100%" />
</template>
</a-table-column>
<a-table-column :title="t('warehouse.common.delete')" :width="72" align="center">
<template #cell="{ rowIndex }">
<a-button type="text" status="danger" size="small" @click="removeLine(box, rowIndex)">
{{ t('warehouse.common.delete') }}
</a-button>
</template>
</a-table-column>
</template>
</a-table>
</div>
</div>
<template #footer>
<a-space :size="8">
<a-button @click="closeModal">{{ t('warehouse.common.cancel') }}</a-button>
<a-button type="primary" :loading="saving" @click="handleSave">{{ t('warehouse.common.save') }}</a-button>
</a-space>
</template>
</ModalPro>
</div>
</template>
<style scoped lang="less">
@import '@/styles/warehouse-list-page.less';
@import '@/styles/warehouse-form-modal.less';
.box-block {
margin-bottom: 16px;
}
.box-title {
font-weight: 500;
margin-bottom: 8px;
}
.wh-form-box-tabs {
margin-bottom: 12px;
}
.receive-box-block {
margin-bottom: 20px;
}
</style>