支持私人地址人工报价及海外入库托盘打印和短少确认

This commit is contained in:
2026-11-04 14:11:26 +08:00
parent 84dd9dcbb4
commit 20bde6c72d
11 changed files with 292 additions and 22 deletions
@@ -7,23 +7,30 @@ import { BusinessDocumentFees } from '@/modules/overseas-business'
import { computed, onMounted, ref } from 'vue'
import { ElMessage } from 'element-plus'
import { useRoute, useRouter } from 'vue-router'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
import AppDialog from '@/shared/components/AppDialog.vue'
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
import RowActions, { type RowActionItem } from '@/shared/components/RowActions.vue'
import { getInboundPlan, moveInboundPallet, type InboundPlanDetailDto } from '../api/inboundPlans'
import { listOverseasLocations, type OverseasLocationDto } from '../api/locations'
import { getPalletLabelPdf } from '../api/pallets'
import { overseasWarehousePermissions } from '../permissions'
import OverseasDetailShell from '../components/OverseasDetailShell.vue'
import InboundShortageConfirmation from '../components/InboundShortageConfirmation.vue'
import { formatOverseasDateTime, inboundPlanStatusLabel, lastMileOrderTypeLabel } from '../lastMilePresentation'
const route = useRoute(); const router = useRouter()
const access = useAccessPreviewStore()
const planNo = computed(() => String(route.params.orderId || ''))
const customerId = computed(() => String(route.query.customerId || ''))
const detail = ref<InboundPlanDetailDto | null>(null); const loading = ref(false); const loadError = ref<ApiErrorDetails | null>(null)
const moveOpen = ref(false); const moveSaving = ref(false); const moveRow = ref<{ palletNo: string; palletVersion: number } | null>(null); const targetLocationCode = ref(''); const moveReason = ref('入库托盘上架'); const locationOptions = ref<OverseasLocationDto[]>([])
const moveLocationsLoading = ref(false); const moveLocationError = ref<ApiErrorDetails | null>(null); const moveError = ref<ApiErrorDetails | null>(null)
const printingPalletId = ref('')
const printError = ref<ApiErrorDetails | null>(null)
const referenceValue = computed(() => detail.value?.planNo || (/^\d+$/.test(planNo.value) ? '—' : planNo.value))
function text(value: unknown) { return String(value ?? '').trim() || '—' }
function receivingWarehouse(code: unknown, name: unknown) {
@@ -47,7 +54,7 @@ const summaryGroups = [
]
const appointmentFields = ['预约状态', '预约送仓时间', '预计装车时间', '排车状态', '创建时间', '更新时间', '备注']
const palletColumns: DataTableColumn[] = [
{ key: 'actions', label: '操作', width: 100, fixed: 'left', slotName: 'actions' },
{ key: 'actions', label: '操作', width: 180, fixed: 'left', slotName: 'actions' },
{ key: 'inboundCode', prop: 'inboundCode', label: '入仓码', minWidth: 150 },
{ key: 'palletNo', prop: 'palletNo', label: '托盘号', minWidth: 150 },
{ key: 'putawayStatus', label: '上架状态', width: 110, prop: 'putawayStatus', afterColumn: 'palletNo' },
@@ -67,11 +74,34 @@ const palletRows = computed(() => (detail.value?.allocations || []).map((allocat
palletNo: text(allocation.palletNo),
palletOwner: allocation.palletOwnedByCurrentPlan ? `${allocation.palletOwnerPlanNo}(本计划)` : `${allocation.palletOwnerPlanNo}(跨计划合托)`,
packageCount: Number(allocation.pickedPackageCount ?? allocation.countedPackageCount ?? allocation.plannedPackageCount ?? 0),
palletId: String(allocation.palletId), palletVersion: pallet?.version ?? null, sourceLocationId: pallet?.warehouseLocationId || null, sourceLocationCode: locationCode,
palletId: String(allocation.palletId), warehousePalletId: pallet?.warehousePalletId || null, palletVersion: pallet?.version ?? null, sourceLocationId: pallet?.warehouseLocationId || null, sourceLocationCode: locationCode,
sourceLocationVersion: null, location: locationCode || (pallet?.warehouseLocationId ? '—' : '虚拟收货位'), locationType: locationType === 'SHARED_OVERFLOW' ? 'A0共享溢出位' : locationType === 'NORMAL' ? '普通库位' : '虚拟收货位', putawayStatus: locationType === 'NORMAL' || locationType === 'SHARED_OVERFLOW' ? '已上架' : '待上架',
}
}))
const availableLocations = computed(() => locationOptions.value.filter(item => item.status !== 'DISABLED'))
function palletActions(row: typeof palletRows.value[number]): RowActionItem[] {
return [
{ key: 'print', label: '打印托盘码', visible: access.can(overseasWarehousePermissions.palletList),
disabled: !!printingPalletId.value || !row.warehousePalletId || !detail.value?.overseasWarehouseId,
loading: !!row.warehousePalletId && printingPalletId.value === row.warehousePalletId },
{ key: 'move', label: '移库', disabled: moveSaving.value || moveLocationsLoading.value || row.palletNo === '—' || row.palletVersion == null },
]
}
async function printPalletLabel(row: typeof palletRows.value[number]) {
const warehouseId = detail.value?.overseasWarehouseId
if (!access.can(overseasWarehousePermissions.palletList) || printingPalletId.value || !warehouseId || !row.warehousePalletId) return
printingPalletId.value = row.warehousePalletId; printError.value = null
try {
const blob = await getPalletLabelPdf(warehouseId, row.warehousePalletId)
const url = URL.createObjectURL(blob)
try {
const link = document.createElement('a')
link.href = url; link.download = `${row.palletNo}-托盘码.pdf`; link.click()
} finally { window.setTimeout(() => URL.revokeObjectURL(url), 0) }
ElMessage.success('已生成 1 张托盘标签 PDF')
} catch (cause) { printError.value = getApiErrorDetails(cause, '托盘标签打印失败,请重试') }
finally { printingPalletId.value = '' }
}
async function openMove(row: typeof palletRows.value[number]) {
if (moveSaving.value || moveLocationsLoading.value || row.palletVersion == null || row.palletNo === '—') return
if (!detail.value?.overseasWarehouseCode) { ElMessage.warning('入库单未返回海外仓范围,暂不能移库'); return }
@@ -99,7 +129,9 @@ onMounted(load)
<template><OverseasDetailShell workbench class="admin-density" dense title="入库单详情" reference-label="入库单号" :reference-value="referenceValue" back-label="返回入库单" :back-text="appointmentReturnTarget(route.query) ? '返回预约工作' : '返回列表'" domain="入库单详情" :pending="false" :status-text="detail ? status(detail.status) : '加载中'" @back="back">
<LoadErrorState v-if="loadError" title="入库单详情加载失败" :message="loadError.message" :loading="loading" @retry="load"><template #details><ApiErrorMeta :details="loadError" /></template></LoadErrorState>
<template v-else><section v-loading="loading" class="detail-section" aria-label="入库单基本信息"><div class="detail-groups"><section v-for="group in summaryGroups" :key="group.title" class="detail-group" :aria-label="group.title"><h2>{{ group.title }}</h2><dl class="detail-fields density-fields"><div v-for="label in group.fields" :key="label"><dt>{{ label }}</dt><dd>{{ fieldValues[label] }}</dd></div></dl></section></div></section>
<section class="detail-section pallet-section" aria-labelledby="inbound-pallet-heading"><div class="section-heading"><div><h2 id="inbound-pallet-heading">入库单托盘明细<InlineHelp title="托盘明细说明"><p>PDA 合托后,参与合托的入库单会分别展示同一托盘及本单对应箱数;A0 为系统共享溢出位。</p></InlineHelp></h2></div><InboundShortageConfirmation v-if="detail" :detail="detail" @refresh="load" /><span class="section-count">{{ palletRows.length }} 条</span></div><div class="detail-table-wrap"><div class="workbench-table"><DataTable :data="palletRows" :columns="palletColumns" column-settings-mode="hide" row-key="id" empty-text="暂无托盘明细"><template #actions="{ row }"><el-button class="density-action" size="small" :disabled="moveSaving || moveLocationsLoading || !(row as typeof palletRows[number]).palletNo || (row as typeof palletRows[number]).palletNo === '—' || (row as typeof palletRows[number]).palletVersion == null" @click="openMove(row as typeof palletRows[number])">移库</el-button></template></DataTable></div></div></section>
<section class="detail-section pallet-section" aria-labelledby="inbound-pallet-heading"><div class="section-heading"><div><h2 id="inbound-pallet-heading">入库单托盘明细<InlineHelp title="托盘明细说明"><p>PDA 合托后,参与合托的入库单会分别展示同一托盘及本单对应箱数;A0 为系统共享溢出位。</p></InlineHelp></h2></div><InboundShortageConfirmation v-if="detail" :detail="detail" @refresh="load" /><span class="section-count">{{ palletRows.length }} 条</span></div><div class="detail-table-wrap">
<LoadErrorState v-if="printError" compact title="托盘标签打印失败" :message="printError.message" :show-retry="false"><template #details><ApiErrorMeta :details="printError" /></template></LoadErrorState>
<div class="workbench-table"><DataTable :data="palletRows" :columns="palletColumns" column-settings-mode="hide" row-key="id" empty-text="暂无托盘明细"><template #actions="{ row }"><RowActions :actions="palletActions(row as typeof palletRows[number])" text-actions @action="action => action === 'print' ? printPalletLabel(row as typeof palletRows[number]) : openMove(row as typeof palletRows[number])" /></template></DataTable></div></div></section>
<section class="detail-section appointment-section" aria-labelledby="inbound-appointment-heading"><div class="compact-section-heading"><h2 id="inbound-appointment-heading">预约与排车</h2></div><dl class="detail-fields appointment-fields density-fields"><div v-for="label in appointmentFields" :key="label" :class="{ 'is-wide': label === '备注' }"><dt>{{ label }}</dt><dd>{{ fieldValues[label] }}</dd></div></dl></section>
<section v-if="detail" class="detail-section fee-section" aria-label="入库单费用"><BusinessDocumentFees business-type="OVERSEAS_INBOUND" :business-no="detail.planNo" :customer-id="detail.customerId" :business-id="detail.id" /></section></template>
</OverseasDetailShell><AppDialog v-model="moveOpen" class="admin-density-dialog admin-workbench is-dialog" :title="`托盘移库 · ${moveRow?.palletNo || '—'}`" width="min(560px, calc(100vw - 28px))" fullscreenable confirm-text="提交移库" :confirm-loading="moveSaving" :confirm-disabled="moveLocationsLoading || !!moveLocationError || !targetLocationCode || !moveReason.trim()" @confirm="submitMove"><div class="density-dialog-sections">