896 lines
32 KiB
Vue
896 lines
32 KiB
Vue
<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>
|