All checks were successful
Build and Deploy Front / deploy (push) Successful in 6m25s
Co-authored-by: Cursor <cursoragent@cursor.com>
1390 lines
46 KiB
Vue
1390 lines
46 KiB
Vue
<script setup lang="ts">
|
||
import { computed, defineAsyncComponent, 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,
|
||
postWarehouseInboundPrintLabels,
|
||
saveWarehouseInbound,
|
||
} from '@/api/warehouse'
|
||
import { getWarehouseList } from '@/api/system/warehouse'
|
||
import { $baseMessage } from '@/utils'
|
||
import { hasPermission } from '@/utils/rbac-permission'
|
||
import { useHiprintClientState } from '@/utils/hiprint/hiprintClientState'
|
||
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' })
|
||
|
||
const OrderPrintModal = defineAsyncComponent(
|
||
() => import('@/components/PrintTemplate/OrderPrintModal.vue'),
|
||
)
|
||
|
||
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()
|
||
const printConnectionState = useHiprintClientState()
|
||
const printVisible = ref(false)
|
||
const printItems = ref<Array<{ id: number | string; label: string; url: string }>>([])
|
||
const printPreparing = ref(false)
|
||
|
||
/** 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 [registerFormModal, { openModal, closeModal }] = useModal() as any
|
||
const [registerReceiveModal, { openModal: openReceiveModal, closeModal: closeReceiveModal }] = useModal() as any
|
||
/** 已完成等不可改状态:同一弹窗只读查看 */
|
||
const formReadonly = ref(false)
|
||
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 formModalTitle = computed(() => {
|
||
if (!formModel.id) return t('warehouse.inbound.modal.add')
|
||
if (formReadonly.value) return t('warehouse.inbound.modal.view')
|
||
return t('warehouse.inbound.modal.edit')
|
||
})
|
||
|
||
const formInboundTypeLabel = computed(() =>
|
||
t(`warehouse.inbound.type.${formModel.inboundType}`, formModel.inboundType || '-'),
|
||
)
|
||
|
||
const modalWarehouseName = computed(() => {
|
||
const wh = warehouseOptions.value.find((w) => w.value === formModel.warehouseId)
|
||
return wh?.label || ''
|
||
})
|
||
|
||
function displayOrDash(value: unknown) {
|
||
const text = String(value ?? '').trim()
|
||
return text || '-'
|
||
}
|
||
|
||
function formatBoxWeight(box: InboundBoxForm) {
|
||
if (box.weightG == null || Number.isNaN(Number(box.weightG))) return '-'
|
||
return `${box.weightG} g`
|
||
}
|
||
|
||
function formatBoxSize(box: InboundBoxForm) {
|
||
const l = box.lengthCm
|
||
const w = box.widthCm
|
||
const h = box.heightCm
|
||
if (l == null && w == null && h == null) return '-'
|
||
return `${l ?? '-'} × ${w ?? '-'} × ${h ?? '-'} cm`
|
||
}
|
||
|
||
function resolveSkuLabel(line: InboundLineForm) {
|
||
if (!line.skuId) return '-'
|
||
const opt = skuOptions.value.find((s) => s.value === line.skuId)
|
||
return opt?.label || line.skuCode || `#${line.skuId}`
|
||
}
|
||
|
||
function viewSkuLines(box: InboundBoxForm) {
|
||
return box.lines.filter((l) => l.skuId)
|
||
}
|
||
|
||
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) => {
|
||
if (formReadonly.value) return
|
||
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 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 canEditInbound(row: any) {
|
||
return row?._status === 'forecasted' || row?._status === 'received'
|
||
}
|
||
|
||
function handleAdd() {
|
||
if (!hasPermission('Warehouse.InboundNotice.Save')) return
|
||
if (isAdmin.value && ensureMutationCustomerId() == null) return
|
||
formReadonly.value = false
|
||
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 openInboundForm(row: any) {
|
||
const editable =
|
||
hasPermission('Warehouse.InboundNotice.Save') && canEditInbound(row)
|
||
if (editable && isAdmin.value && ensureMutationCustomerId() == null) return
|
||
try {
|
||
const detail = await getWarehouseInboundDetail(
|
||
row._raw?.id || row.id,
|
||
isAdmin.value ? resolveSelectedCustomerId() : undefined,
|
||
)
|
||
const normalized = normalizeListRow(detail || {})
|
||
formReadonly.value = !editable
|
||
formModel.id = Number(pickField(normalized, 'id', 'Id') || 0)
|
||
formModel.warehouseId = pickField<number>(normalized, 'warehouseId', 'WarehouseId')
|
||
formModel.markReference = String(pickField(normalized, 'markReference', 'MarkReference') ?? '')
|
||
formModel.inboundType = String(pickField(normalized, 'inboundType', 'InboundType') ?? 'normal')
|
||
formModel.remark = String(pickField(normalized, 'remark', 'Remark') ?? '')
|
||
|
||
const boxesRaw = pickField(normalized, 'boxes', 'Boxes')
|
||
const boxes = Array.isArray(boxesRaw) ? boxesRaw : []
|
||
if (!boxes.length) {
|
||
if (editable) resetBoxes()
|
||
else {
|
||
boxRows.value = []
|
||
activeBoxKey.value = ''
|
||
}
|
||
} else {
|
||
boxRows.value = boxes.map((box: any, index: number) => {
|
||
const b = normalizeListRow(box)
|
||
const linesRaw = pickField(b, 'lines', 'Lines')
|
||
const lines = Array.isArray(linesRaw) ? linesRaw : []
|
||
const mappedLines: InboundLineForm[] = lines.map((line: any) => {
|
||
const l = normalizeListRow(line)
|
||
return {
|
||
skuId: Number(pickField(l, 'skuId', 'SkuId') || 0) || undefined,
|
||
skuCode: String(pickField(l, 'skuCode', 'SkuCode') ?? ''),
|
||
plannedQty: Number(pickField(l, 'plannedQty', 'PlannedQty') ?? 1) || 1,
|
||
}
|
||
})
|
||
if (editable && (!mappedLines.length || mappedLines.every((l) => l.skuId))) {
|
||
mappedLines.push({ skuId: undefined, skuCode: '', plannedQty: 1 })
|
||
}
|
||
return {
|
||
key: `box-open-${pickField(b, 'id', 'Id') || index}-${Date.now()}`,
|
||
boxSeqNo: String(pickField(b, 'boxSeqNo', 'BoxSeqNo') ?? index + 1),
|
||
markBoxNo: String(pickField(b, 'markBoxNo', 'MarkBoxNo') ?? ''),
|
||
weightG: pickField<number>(b, 'weightG', 'WeightG') ?? undefined,
|
||
lengthCm: pickField<number>(b, 'lengthCm', 'LengthCm') ?? undefined,
|
||
widthCm: pickField<number>(b, 'widthCm', 'WidthCm') ?? undefined,
|
||
heightCm: pickField<number>(b, 'heightCm', 'HeightCm') ?? undefined,
|
||
lines: mappedLines.length
|
||
? mappedLines
|
||
: [{ skuId: undefined, skuCode: '', plannedQty: 1 }],
|
||
}
|
||
})
|
||
selectedBoxKeys.value = []
|
||
activeBoxKey.value = boxRows.value[0]?.key || ''
|
||
}
|
||
|
||
if (!formModel.warehouseId) {
|
||
$baseMessage(t('warehouse.common.noCurrentWarehouse'), 'warning')
|
||
return
|
||
}
|
||
openModal(true)
|
||
} catch (e: any) {
|
||
$baseMessage(e?.message || t('warehouse.inbound.message.detailFailed'), 'error')
|
||
}
|
||
}
|
||
|
||
function closeFormModal() {
|
||
formReadonly.value = false
|
||
closeModal()
|
||
}
|
||
|
||
async function handleSave() {
|
||
if (formReadonly.value) return
|
||
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')
|
||
closeFormModal()
|
||
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 isUsablePrintUrl(raw: unknown) {
|
||
const value = String(raw ?? '').trim()
|
||
if (!value) return false
|
||
if (value.includes('获取中')) return false
|
||
return true
|
||
}
|
||
|
||
async function handlePrintLabel(row: any) {
|
||
if (!hasPermission('Warehouse.InboundNotice.PrintLabel')) return
|
||
const id = Number(row?._raw?.id || row?.id || 0)
|
||
if (!id) return
|
||
|
||
printVisible.value = false
|
||
printItems.value = []
|
||
printPreparing.value = true
|
||
try {
|
||
const customerId = isAdmin.value ? resolveSelectedCustomerId() ?? undefined : undefined
|
||
const { printUrl, noticeNo, pageCount } = await postWarehouseInboundPrintLabels({
|
||
id,
|
||
customerId,
|
||
})
|
||
if (!isUsablePrintUrl(printUrl)) {
|
||
$baseMessage(t('warehouse.inbound.message.printEmpty'), 'warning')
|
||
return
|
||
}
|
||
const label = noticeNo || row.noticeNo || String(id)
|
||
printItems.value = [
|
||
{
|
||
id,
|
||
label: pageCount > 1 ? `${label}(${pageCount}页)` : label,
|
||
url: printUrl,
|
||
},
|
||
]
|
||
printVisible.value = true
|
||
if (!printConnectionState.enabled || !printConnectionState.connected) {
|
||
$baseMessage('已打开箱唛预览;静默打印需先在顶部连接打印开关', 'info')
|
||
}
|
||
} catch (e: any) {
|
||
$baseMessage(e?.message || t('warehouse.inbound.message.printFailed'), 'error')
|
||
} finally {
|
||
printPreparing.value = false
|
||
}
|
||
}
|
||
|
||
function actionRenderer(row: any) {
|
||
const canEdit = hasPermission('Warehouse.InboundNotice.Save') && canEditInbound(row)
|
||
const parts = [
|
||
canEdit
|
||
? renderWarehouseActionBtn('open', t('warehouse.common.edit'), 'edit')
|
||
: renderWarehouseActionBtn('open', t('warehouse.common.detail'), 'view'),
|
||
]
|
||
if (hasPermission('Warehouse.InboundNotice.PrintLabel')) {
|
||
parts.push(
|
||
renderWarehouseActionBtn('printLabel', t('warehouse.inbound.actions.printLabel'), 'print'),
|
||
)
|
||
}
|
||
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 === 'open') void openInboundForm(row)
|
||
if (action === 'printLabel') void handlePrintLabel(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>
|
||
|
||
<OrderPrintModal
|
||
v-if="printVisible"
|
||
v-model:visible="printVisible"
|
||
mode="single"
|
||
:items="printItems"
|
||
:preparing="printPreparing"
|
||
/>
|
||
|
||
<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="formModalTitle"
|
||
:width="860"
|
||
:mask-closable="false"
|
||
@register="registerFormModal"
|
||
@cancel="closeFormModal"
|
||
>
|
||
<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 v-if="!formReadonly" :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>
|
||
<div v-if="formReadonly" class="inbound-view-basic">
|
||
<div class="inbound-view-row">
|
||
<div class="inbound-view-field inbound-view-field--grow">
|
||
<div class="inbound-view-label">{{ t('warehouse.inbound.form.markReference') }}</div>
|
||
<div class="wh-form-readonly">{{ displayOrDash(formModel.markReference) }}</div>
|
||
</div>
|
||
<div class="inbound-view-field">
|
||
<div class="inbound-view-label">{{ t('warehouse.inbound.table.inboundType') }}</div>
|
||
<div class="wh-form-readonly">{{ formInboundTypeLabel }}</div>
|
||
</div>
|
||
</div>
|
||
<div class="inbound-view-field">
|
||
<div class="inbound-view-label">{{ t('warehouse.parcel.form.remark') }}</div>
|
||
<div class="wh-form-readonly">{{ displayOrDash(formModel.remark) }}</div>
|
||
</div>
|
||
</div>
|
||
<a-form v-else :model="formModel" layout="vertical" class="inbound-form-basic edit-form">
|
||
<a-row :gutter="16">
|
||
<a-col :span="14">
|
||
<a-form-item :required="false">
|
||
<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 v-if="!formReadonly" :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 v-if="formReadonly" class="inbound-box-meta inbound-box-meta--view">
|
||
<div class="inbound-box-meta__field inbound-box-meta__field--mark">
|
||
<span class="inbound-box-meta__label">{{ t('warehouse.inbound.form.markBoxNo') }}</span>
|
||
<div class="wh-form-readonly">{{ displayOrDash(box.markBoxNo) }}</div>
|
||
</div>
|
||
<div class="inbound-box-meta__field inbound-box-meta__field--weight">
|
||
<span class="inbound-box-meta__label">{{ t('warehouse.inbound.form.boxWeight') }}</span>
|
||
<div class="wh-form-readonly">{{ formatBoxWeight(box) }}</div>
|
||
</div>
|
||
<div class="inbound-box-meta__field inbound-box-meta__field--size">
|
||
<span class="inbound-box-meta__label">{{ t('warehouse.inbound.form.boxSize') }}</span>
|
||
<div class="wh-form-readonly">{{ formatBoxSize(box) }}</div>
|
||
</div>
|
||
</div>
|
||
<div v-else 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 v-if="!formReadonly" 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
|
||
v-if="formReadonly"
|
||
class="inbound-sku-table"
|
||
:data="viewSkuLines(box)"
|
||
: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 }">
|
||
<span class="wh-form-readonly">{{ resolveSkuLabel(record) }}</span>
|
||
</template>
|
||
</a-table-column>
|
||
<a-table-column :title="t('warehouse.inbound.line.plannedQty')" :width="120">
|
||
<template #cell="{ record }">
|
||
<span class="wh-form-readonly">{{ record.plannedQty }}</span>
|
||
</template>
|
||
</a-table-column>
|
||
</template>
|
||
</a-table>
|
||
<a-table
|
||
v-else
|
||
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="closeFormModal">{{ t('warehouse.common.cancel') }}</a-button>
|
||
<a-button
|
||
v-if="!formReadonly"
|
||
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';
|
||
|
||
.wh-form-box-tabs {
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.inbound-view-basic {
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.inbound-view-row {
|
||
display: flex;
|
||
gap: 24px;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.inbound-view-field {
|
||
min-width: 140px;
|
||
|
||
&--grow {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
}
|
||
|
||
.inbound-view-label {
|
||
margin-bottom: 4px;
|
||
font-size: 13px;
|
||
color: var(--color-text-3);
|
||
}
|
||
|
||
.inbound-box-meta--view {
|
||
.inbound-box-meta__label {
|
||
margin-bottom: 4px;
|
||
}
|
||
}
|
||
.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>
|