front/src/views/warehouse/inboundNotice/index.vue
zzh 33f696bc42
All checks were successful
Build and Deploy Front / deploy (push) Successful in 7m24s
feat: 抽取 PublishDiff 公共组件,完善发布/订单/渠道与多模块体验
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-13 23:34:54 +08:00

1141 lines
37 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, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { Modal } from '@arco-design/web-vue'
import { IconDelete, IconPlus, IconQuestionCircle } from '@arco-design/web-vue/es/icon'
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: t(tab.labelKey),
badge: tab.countKey ? summary.value[tab.countKey] ?? 0 : undefined,
})),
)
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) => {
const filled = box.lines.filter((l) => l.skuId).length
return {
key: box.key,
label:
filled > 0
? t('warehouse.inbound.form.boxTabWithSku', { n: box.boxSeqNo, sku: filled })
: `${t('warehouse.inbound.box')} ${box.boxSeqNo}`,
}
}),
)
const canRemoveBox = computed(() => boxRows.value.length > 1)
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) {
$baseMessage(t('warehouse.inbound.message.keepOneBox'), 'warning')
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 removeLine(box: InboundBoxForm, index: number) {
if (box.lines.length <= 1) {
box.lines[0] = { skuId: undefined, skuCode: '', plannedQty: 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 || ''
}
watch(
boxRows,
(boxes) => {
for (const box of boxes) {
const last = box.lines[box.lines.length - 1]
if (last?.skuId) {
box.lines.push({ skuId: undefined, skuCode: '', plannedQty: 1 })
}
}
},
{ deep: true },
)
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,
}
}
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"
@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="form-section-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 }"
:scroll="{ x: '100%', y: 220 }"
>
<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" hide-button 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="860"
:mask-closable="false"
@register="registerFormModal"
@cancel="closeModal"
>
<div class="wh-form-modal">
<div class="wh-form-info-bar">
<span class="label-with-tip">
{{ t('warehouse.parcel.form.currentWarehouse') }}:{{
modalWarehouseName
|| warehouseOptions.find((o) => o.value === formModel.warehouseId)?.label
|| currentWarehouseStore.warehouseName
|| formModel.warehouseId
|| '-'
}}
<a-tooltip :content="t('warehouse.inbound.tip.currentWarehouse')">
<icon-question-circle class="tip-icon" />
</a-tooltip>
</span>
</div>
<div class="form-section-title">{{ t('warehouse.modal.section.basic') }}</div>
<a-form :model="formModel" layout="vertical" class="inbound-form-basic edit-form">
<a-row :gutter="16">
<a-col :span="14">
<a-form-item>
<template #label>
<span class="label-with-tip">
{{ t('warehouse.inbound.form.markReference') }}
<a-tooltip :content="t('warehouse.inbound.tip.markReference')">
<icon-question-circle class="tip-icon" />
</a-tooltip>
</span>
</template>
<a-input
v-model="formModel.markReference"
:placeholder="t('warehouse.inbound.form.markPlaceholder')"
allow-clear
/>
</a-form-item>
</a-col>
<a-col :span="10">
<a-form-item>
<template #label>
<span class="label-with-tip">
{{ t('warehouse.inbound.table.inboundType') }}
<a-tooltip :content="t('warehouse.inbound.tip.inboundType')">
<icon-question-circle class="tip-icon" />
</a-tooltip>
</span>
</template>
<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-col>
</a-row>
<a-form-item>
<template #label>{{ t('warehouse.parcel.form.remark') }}</template>
<a-input
v-model="formModel.remark"
allow-clear
:placeholder="t('warehouse.inbound.form.remarkPlaceholder')"
/>
</a-form-item>
</a-form>
<div class="form-section-title form-section-title--with-extra">
<span class="label-with-tip">
{{ t('warehouse.inbound.form.boxSkuDetail') }}
<span class="wh-form-section__count">
{{ t('warehouse.inbound.form.summaryMeta', {
box: boxCount,
sku: totalSkuCount,
qty: totalPlannedQty,
}) }}
</span>
<a-tooltip :content="t('warehouse.inbound.tip.boxSkuDetail')">
<icon-question-circle class="tip-icon" />
</a-tooltip>
</span>
<a-space :size="8">
<a-button size="mini" @click="addBox">
<template #icon><IconPlus :size="12" /></template>
{{ t('warehouse.inbound.actions.addBox') }}
</a-button>
<a-button size="mini" @click="addBoxes(5)">
{{ t('warehouse.inbound.actions.addBox5') }}
</a-button>
<a-button
size="mini"
status="danger"
:disabled="!canRemoveBox"
@click="removeActiveBox"
>
{{ t('warehouse.inbound.actions.removeCurrentBox') }}
</a-button>
</a-space>
</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 inbound-box-panel"
>
<div class="inbound-box-meta">
<div class="inbound-box-meta__field inbound-box-meta__field--mark">
<span class="inbound-box-meta__label label-with-tip">
{{ t('warehouse.inbound.form.markBoxNo') }}
<a-tooltip :content="t('warehouse.inbound.tip.markBoxNo')">
<icon-question-circle class="tip-icon" />
</a-tooltip>
</span>
<a-input
v-model="box.markBoxNo"
allow-clear
:placeholder="t('warehouse.inbound.form.markBoxNoPlaceholder')"
/>
</div>
<div class="inbound-box-meta__field inbound-box-meta__field--weight">
<span class="inbound-box-meta__label">{{ t('warehouse.inbound.form.boxWeight') }}</span>
<a-input-number
v-model="box.weightG"
:min="0"
hide-button
style="width: 100%"
:placeholder="t('warehouse.inbound.form.optional')"
>
<template #suffix>g</template>
</a-input-number>
</div>
<div class="inbound-box-meta__field inbound-box-meta__field--size">
<span class="inbound-box-meta__label">{{ t('warehouse.inbound.form.boxSize') }}(cm)</span>
<div class="inbound-box-size">
<a-input-number
v-model="box.lengthCm"
:min="0"
hide-button
:placeholder="t('warehouse.sku.form.length')"
>
<template #suffix>cm</template>
</a-input-number>
<span>×</span>
<a-input-number
v-model="box.widthCm"
:min="0"
hide-button
:placeholder="t('warehouse.sku.form.width')"
>
<template #suffix>cm</template>
</a-input-number>
<span>×</span>
<a-input-number
v-model="box.heightCm"
:min="0"
hide-button
:placeholder="t('warehouse.sku.form.height')"
>
<template #suffix>cm</template>
</a-input-number>
</div>
</div>
</div>
<div class="inbound-sku-head">
<span class="wh-form-hint label-with-tip">
{{ t('warehouse.inbound.form.autoAddSkuRow') }}
<a-tooltip :content="t('warehouse.inbound.tip.skuLines')">
<icon-question-circle class="tip-icon" />
</a-tooltip>
</span>
</div>
<a-table
class="inbound-sku-table"
:data="box.lines"
:pagination="false"
size="small"
:bordered="{ cell: true }"
:scroll="{ x: '100%', y: 240 }"
>
<template #columns>
<a-table-column :title="'#'" :width="44">
<template #cell="{ rowIndex }">{{ rowIndex + 1 }}</template>
</a-table-column>
<a-table-column :title="t('warehouse.inbound.form.sku')" :min-width="220">
<template #cell="{ record }">
<a-select
v-model="record.skuId"
:options="skuOptions"
allow-search
allow-clear
: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="120">
<template #cell="{ record }">
<a-input-number
v-model="record.plannedQty"
:min="1"
hide-button
style="width: 100%"
/>
</template>
</a-table-column>
<a-table-column :title="t('common.actions')" :width="64" align="center">
<template #cell="{ rowIndex, record }">
<div class="inbound-action-cell">
<a-button
v-if="record.skuId || box.lines.length > 1"
type="text"
status="danger"
size="mini"
:aria-label="t('warehouse.common.delete')"
@click="removeLine(box, rowIndex)"
>
<IconDelete :size="16" />
</a-button>
</div>
</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;
}
.form-section-title {
margin: 0 0 12px;
padding: 0 0 8px;
font-size: 13px;
font-weight: 600;
color: var(--color-text-2);
border-bottom: 1px solid var(--color-border-2);
&:not(:first-child) {
margin-top: 8px;
}
&--with-extra {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
flex-wrap: wrap;
}
}
.label-with-tip {
display: inline-flex;
align-items: center;
gap: 4px;
}
.tip-icon {
color: var(--color-text-3);
cursor: help;
font-size: 14px;
}
.wh-form-section__count {
margin-left: 8px;
font-size: 12px;
font-weight: 400;
color: var(--color-text-3);
}
.inbound-form-basic {
:deep(.arco-form-item) {
margin-bottom: 16px;
}
}
.inbound-box-panel {
padding: 12px;
}
.inbound-box-meta {
display: flex;
flex-wrap: wrap;
gap: 12px 16px;
margin-bottom: 12px;
align-items: flex-end;
}
.inbound-box-meta__field {
display: flex;
flex-direction: column;
gap: 4px;
min-width: 0;
&--mark {
flex: 1 1 160px;
}
&--weight {
flex: 0 0 120px;
}
&--size {
flex: 1 1 320px;
}
}
.inbound-box-meta__label {
font-size: 12px;
color: var(--color-text-3);
line-height: 1.2;
}
.inbound-box-size {
display: flex;
align-items: center;
gap: 6px;
:deep(.arco-input-number) {
width: 96px;
}
> span {
color: var(--color-text-3);
}
}
.inbound-sku-head {
display: flex;
justify-content: flex-end;
margin-bottom: 8px;
}
.inbound-sku-table {
:deep(.arco-table-td) {
vertical-align: middle;
}
:deep(.arco-table-cell) {
padding-top: 6px;
padding-bottom: 6px;
}
}
.inbound-action-cell {
display: flex;
align-items: center;
justify-content: center;
min-height: 32px;
}
</style>