Files
admin-web/src/modules/overseas-warehouse/pages/InboundOrdersView.vue
T

278 lines
27 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 '@/styles/adminWorkbench.css'
import '@/styles/adminDensity.css'
import { computed, onMounted, reactive, ref } from 'vue'
import { CalendarCheck2, Truck } from '@lucide/vue'
import { ElMessage } from 'element-plus'
import { onBeforeRouteLeave, onBeforeRouteUpdate, useRouter } from 'vue-router'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
import AdminPage from '@/shared/components/AdminPage.vue'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
import AppDialog from '@/shared/components/AppDialog.vue'
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
import ListPageLayout from '@/shared/components/ListPageLayout.vue'
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
import OperationErrorToast from '@/shared/components/OperationErrorToast.vue'
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
import SmartDispatchDialog from '../components/SmartDispatchDialog.vue'
import FbaAppointmentFields from '../components/FbaAppointmentFields.vue'
import type { PlanningSourceLookup } from '@/modules/appointment-calendar'
import { createAppointmentForm, appointmentPayload, appointmentFormError } from '../appointmentForm'
import { createFbaAppointment, getFbaAppointment, listInboundOrders, updateFbaAppointment, updateInboundPlan, type InboundOrderSummaryDto } from '../api/inboundOrders'
import { listReceivingOverseasWarehouses, type ReceivingOverseasWarehouseDto } from '../api/pallets'
import { overseasWarehousePermissions as permission } from '../permissions'
import { formatOverseasDateTime, inboundPlanStatusLabel, lastMileOrderTypeLabel } from '../lastMilePresentation'
import type { InboundOrderListItem } from '../types'
const access = useAccessPreviewStore()
const canCreateDispatch = computed(() => access.can(permission.dispatchList) && access.can(permission.dispatchCreate))
const router = useRouter()
const rows = ref<InboundOrderListItem[]>([])
const selectedRows = ref<InboundOrderListItem[]>([])
const loading = ref(false)
const saving = ref(false)
const loadError = ref<ApiErrorDetails | null>(null)
const mutationError = ref<ApiErrorDetails | null>(null)
const page = ref(1)
const pageSize = ref(10)
const total = ref(0)
const filters = ref<QueryModel>({ orderNo: '', inboundCode: '', lxCode: '', status: '', warehouse: '', fbaWarehouse: '', appointmentStatus: '', dispatchStatus: '', inboundDate: [] })
const fields = computed<QueryField[]>(() => [
{ key: 'orderNo', type: 'input', label: '入库单号', width: 165, placeholder: '入库单号' },
{ key: 'inboundCode', type: 'input', label: '入仓码', width: 145, placeholder: '入仓码' },
{ key: 'lxCode', type: 'input', label: 'LX代码', width: 160, placeholder: '领星入库单代码' },
{ key: 'status', type: 'select', label: '状态', width: 130, placeholder: '全部状态', options: [{ label: '待入库', value: 'PENDING_ASSIGNMENT' }, { label: '已签收', value: 'SIGNED' }, { label: '拣货中', value: 'PICKING' }, { label: '全部入库', value: 'COMPLETED' }, { label: '部分入库', value: 'PARTIAL_INBOUND' }, { label: '待差异核对', value: 'PENDING_RECONCILIATION' }] },
{ key: 'warehouse', type: 'input', label: '海外仓', width: 155, placeholder: '全部海外仓' },
{ key: 'fbaWarehouse', type: 'input', label: 'FBA仓库', width: 145, placeholder: '全部FBA仓库' },
{ key: 'appointmentStatus', type: 'select', label: '预约状态', width: 145, placeholder: '全部预约状态', options: [{ label: '未预约', value: 'UNAPPOINTED' }, { label: '预约中', value: 'PENDING' }, { label: '预约成功', value: 'SUCCESS' }, { label: '预约失败', value: 'FAILED' }, { label: '已取消', value: 'CANCELLED' }, { label: '多批预约', value: 'MULTIPLE' }] },
{ key: 'dispatchStatus', type: 'select', label: '排车状态', width: 140, placeholder: '全部排车状态', options: [{ label: '未排车', value: 'UNSCHEDULED' }, { label: '已排车', value: 'SCHEDULED' }] },
{ key: 'inboundDate', type: 'daterange', label: '入库时间', width: 260 },
])
const appointmentOpen = ref(false)
const smartDispatchOpen = ref(false)
const appointmentLoading = ref(false)
const appointmentMode = ref<'create' | 'edit'>('create')
const editingAppointmentNo = ref('')
const appointmentSelectedCount = ref(0)
const appointmentFbaWarehouse = ref('—')
const appointmentVersion = ref(0)
const appointmentForm = reactive(createAppointmentForm())
const appointmentTimezone = ref<string | null>(null)
const appointmentSources = ref<PlanningSourceLookup[]>([])
const confirmationReady = ref(false)
const appointmentFields = ref<InstanceType<typeof FbaAppointmentFields> | null>(null)
const pendingAppointment = ref<{appointmentNo: string | null; version: number; originWarehouseId: string; fbaWarehouseCode: string; lineIds: string[]; details: ReturnType<typeof appointmentPayload>; idempotencyKey: string} | null>(null)
const appointmentViewOnly = ref(false)
const appointmentReplacementNos = ref<string[]>([])
const appointmentReadOnly = computed(() => appointmentViewOnly.value || !access.can(permission.inboundOrderAppointment))
const appointmentTitle = computed(() => `${appointmentReadOnly.value ? '查看预约' : appointmentMode.value === 'edit' ? '编辑预约' : '预约'} · ${editingAppointmentNo.value || `${appointmentSelectedCount.value} 条入库单`}`)
const planEditOpen = ref(false); const planEditLoading = ref(false); const planEditSaving = ref(false); const planEditTarget = ref<InboundOrderListItem | null>(null); const planEditForm = reactive({ warehouseId: '', warehouseCode: '', warehouseName: '', version: 0 }); const warehouses = ref<ReceivingOverseasWarehouseDto[]>([])
function selectableRow(source: object) {
const row = source as InboundOrderListItem
if (!row.fbaWarehouse || row.fbaWarehouse === '—') return false
const anchor = selectedRows.value[0]
return !anchor || (anchor.fbaWarehouse === row.fbaWarehouse && anchor.originWarehouseId === row.originWarehouseId)
}
const columns: DataTableColumn[] = [
{ key: 'actions', label: '操作', width: 300, fixed: 'left', slotName: 'actions' },
{ key: 'selection', type: 'selection', width: 48, fixed: 'left', selectable: selectableRow },
{ key: 'orderNo', prop: 'orderNo', label: '入库单号', minWidth: 170 },
{ key: 'inboundCode', prop: 'inboundCode', label: '入仓码', minWidth: 140 },
{ key: 'lxCode', prop: 'lxCode', label: 'LX代码', minWidth: 150 },
{ key: 'type', prop: 'type', label: '类型', width: 100 },
{ key: 'warehouse', prop: 'warehouse', label: '海外仓', minWidth: 170 },
{ key: 'fbaWarehouse', prop: 'fbaWarehouse', label: 'FBA仓库', minWidth: 130 },
{ key: 'owner', prop: 'owner', label: '货主', minWidth: 150 },
{ key: 'cartons', prop: 'cartons', label: '应拣货数', width: 100, align: 'right' },
{ key: 'pickedCartons', prop: 'pickedCartons', label: '实际拣货数', width: 110, align: 'right' },
{ key: 'grossWeight', prop: 'grossWeight', label: '货物毛重(kg)', width: 120, align: 'right' },
{ key: 'netWeight', prop: 'netWeight', label: '货物净重(kg)', width: 120, align: 'right' },
{ key: 'volume', prop: 'volume', label: '计费体积(m³)', width: 125, align: 'right' },
{ key: 'pallets', prop: 'pallets', label: '总托盘数', width: 100, align: 'right' },
{ key: 'assigned', prop: 'assigned', label: '分配', width: 82, align: 'center' },
{ key: 'mergeAllowed', prop: 'mergeAllowed', label: '合托', width: 96, align: 'center' },
{ key: 'urgent', prop: 'urgent', label: '加急', width: 82, align: 'center' },
{ key: 'status', prop: 'status', label: '状态', width: 100 },
{ key: 'appointmentStatus', prop: 'appointmentStatus', label: 'FBA预约', width: 110 },
{ key: 'dispatchStatus', prop: 'dispatchStatus', label: '排车状态', width: 110 },
{ key: 'createdAt', prop: 'createdAt', label: '创建时间', minWidth: 170 },
]
function text(value: unknown) { const normalized = String(value ?? '').trim(); return normalized || '—' }
function receivingWarehouse(code: unknown, name: unknown) {
const normalizedCode = String(code ?? '').trim()
const normalizedName = String(name ?? '').trim()
return normalizedCode && normalizedName ? `${normalizedCode} · ${normalizedName}` : '—'
}
function number(value: unknown) { const normalized = Number(value); return Number.isFinite(normalized) ? normalized : 0 }
function typeLabel(value: unknown) { return lastMileOrderTypeLabel(value) }
function appointmentStatusLabel(value: unknown) { return !value ? '未预约' : ({ PENDING: '预约中', SUCCESS: '预约成功', FAILED: '预约失败', CANCELLED: '已取消', MULTIPLE: '多批预约' } as Record<string, string>)[String(value)] || text(value) }
function toListItem(dto: InboundOrderSummaryDto): InboundOrderListItem {
return {
lineId: String(dto.lineId), planId: String(dto.planId), customerId: String(dto.customerId), orderId: String(dto.orderId),
originWarehouseId: String(dto.overseasWarehouseId || ''), warehouseId: text(dto.warehouseId) === '—' ? '' : String(dto.warehouseId), destinationWarehouseCode: text(dto.warehouseCode), destinationWarehouseName: text(dto.warehouseName),
orderNo: text(dto.planNo), inboundCode: text(dto.inboundCode), lxCode: text(dto.lxCode), type: typeLabel(dto.orderType), warehouse: receivingWarehouse(dto.overseasWarehouseCode, dto.overseasWarehouseName),
fbaWarehouse: text(dto.fbaWarehouseCode), owner: text(dto.customerName), cartons: number(dto.packageCount), pickedCartons: number(dto.pickedPackageCount),
grossWeight: number(dto.grossWeightKg), netWeight: number(dto.netWeightKg), volume: number(dto.volumeCbm), pallets: number(dto.palletCount),
assigned: dto.assigned ? '已分配' : '未分配', mergeAllowed: dto.mergeAllowed ? '可合托' : '不合托', urgent: dto.urgent ? '加急' : '普通',
status: inboundPlanStatusLabel(dto.status), appointmentNo: text(dto.appointmentNo), appointmentStatus: appointmentStatusLabel(dto.appointmentStatus),
appointmentAt: formatOverseasDateTime(dto.appointmentAt), expectedLoadingAt: formatOverseasDateTime(dto.expectedLoadingAt), dispatchStatus: dto.dispatchStatus === 'SCHEDULED' ? '已排车' : '未排车',
createdAt: formatOverseasDateTime(dto.createdAt), version: number(dto.version),
}
}
function onSelectionChange(selection: object[]) { selectedRows.value = selection as InboundOrderListItem[] }
function selectionError() {
if (!selectedRows.value.length) return '请至少选择 1 条入库单'
if (selectedRows.value.some(row => row.appointmentStatus === '多批预约')) return '所选货物已有多批预约,请从“管理多批预约”逐批处理,不能重新按整票创建'
if (selectedRows.value.some((row) => !row.fbaWarehouse || row.fbaWarehouse === '—')) return '所选入库单缺少 FBA 仓库信息'
if (selectedRows.value.some(row => !row.originWarehouseId) || new Set(selectedRows.value.map(row => row.originWarehouseId)).size !== 1) return '所选入库单必须属于同一接仓点'
if (new Set(selectedRows.value.map((row) => row.fbaWarehouse)).size !== 1) return '所选入库单必须属于同一 FBA 仓库'
return ''
}
async function openAppointment() {
if (!access.can(permission.inboundOrderAppointment) || !await canLeaveAppointment()) return
appointmentViewOnly.value = false
appointmentReplacementNos.value = []
const error = selectionError()
if (error) { ElMessage.warning(error); return }
appointmentMode.value = 'create'; editingAppointmentNo.value = ''
confirmationReady.value = false
appointmentSources.value = selectedRows.value.map(row => ({ orderId: row.orderId, customerId: row.customerId, destinationKey: `FBA:${row.fbaWarehouse}` }))
appointmentSelectedCount.value = selectedRows.value.length; appointmentFbaWarehouse.value = selectedRows.value[0]?.fbaWarehouse || '—'
appointmentVersion.value = 0
Object.assign(appointmentForm, createAppointmentForm()); appointmentTimezone.value = null; pendingAppointment.value = null
mutationError.value = null
appointmentOpen.value = true
}
function openSmartDispatch() {
if (!canCreateDispatch.value) return
smartDispatchOpen.value = true
}
async function openAppointmentEdit(source: object, readOnly = false) {
if (!await canLeaveAppointment()) return
const row = source as InboundOrderListItem
if (row.appointmentStatus === '多批预约') { openMultipleAppointments(row); return }
if (!row.appointmentNo || row.appointmentNo === '—') return
appointmentViewOnly.value = readOnly
appointmentMode.value = 'edit'; editingAppointmentNo.value = row.appointmentNo
appointmentLoading.value = true; mutationError.value = null; appointmentOpen.value = true
try {
const detail = await getFbaAppointment(row.appointmentNo)
appointmentReplacementNos.value = detail.replacementAppointmentNos || []
appointmentViewOnly.value = readOnly || detail.appointmentStatus === 'CANCELLED' || !!appointmentReplacementNos.value.length || !!detail.replacementBatchKeys?.length
appointmentSelectedCount.value = detail.selectedCount
appointmentFbaWarehouse.value = detail.fbaWarehouseCode || row.fbaWarehouse
appointmentVersion.value = detail.version
confirmationReady.value = false
appointmentSources.value = detail.items.map(item => ({ sourceKey: item.sourceKey, customerId: item.customerId, destinationKey: item.destinationKey || `FBA:${item.fbaWarehouseCode}` }))
Object.assign(appointmentForm, createAppointmentForm(detail)); appointmentTimezone.value = detail.timezone || null; pendingAppointment.value = null
} catch (caught) {
appointmentOpen.value = false
mutationError.value = getApiErrorDetails(caught, 'FBA 预约详情加载失败,请重试')
} finally { appointmentLoading.value = false }
}
async function openMultipleAppointments(row: InboundOrderListItem) {
if (!await canLeaveAppointment() || !access.can('platform:appointment:list')) return
if (!row.orderId || !row.customerId || !row.fbaWarehouse || row.fbaWarehouse === '—') { ElMessage.warning('原货源或客户范围缺失,请重新读取入库单'); return }
void router.push({ path: '/appointment-calendar', query: { orderId: row.orderId, customerId: row.customerId, destinationKey: `FBA:${row.fbaWarehouse}` } })
}
async function canLeaveAppointment() {
if (!appointmentOpen.value) return true
if (saving.value || pendingAppointment.value) { ElMessage.warning('请先核实本次预约保存结果,再关闭或切换'); return false }
return await appointmentFields.value?.canLeave?.() ?? true
}
async function closeAppointment() { if (await canLeaveAppointment()) appointmentOpen.value = false }
async function beforeCloseAppointment(done: () => void) { if (await canLeaveAppointment()) { appointmentOpen.value = false; done() } }
onBeforeRouteLeave(canLeaveAppointment)
onBeforeRouteUpdate(canLeaveAppointment)
async function submitAppointment() {
if (appointmentReadOnly.value || saving.value || appointmentLoading.value) return
if (appointmentMode.value === 'create') { const error = selectionError(); if (error) { ElMessage.warning(error); return } }
const validation = appointmentFormError(appointmentForm)
if (!pendingAppointment.value && validation) { ElMessage.warning(validation); return }
if (!pendingAppointment.value && appointmentForm.appointmentStatus === 'SUCCESS' && !confirmationReady.value) { ElMessage.warning('请先在“正式确认前”区域补齐并保存每票货物资料,再保存预约安排'); return }
if (!pendingAppointment.value && appointmentFields.value?.canLeave && !await appointmentFields.value.canLeave()) return
if (saving.value || appointmentReadOnly.value) return
saving.value = true; mutationError.value = null
try {
pendingAppointment.value ||= {appointmentNo: appointmentMode.value === 'edit' ? editingAppointmentNo.value : null, version: appointmentVersion.value, originWarehouseId: selectedRows.value[0]?.originWarehouseId || '', fbaWarehouseCode: selectedRows.value[0]?.fbaWarehouse || '', lineIds: selectedRows.value.map(row => row.lineId), details: appointmentPayload(appointmentForm), idempotencyKey: crypto.randomUUID()}
const payload = pendingAppointment.value
if (payload.appointmentNo) await updateFbaAppointment(payload.appointmentNo, { details: payload.details, version: payload.version, idempotencyKey: payload.idempotencyKey })
else await createFbaAppointment({ originWarehouseId: payload.originWarehouseId, fbaWarehouseCode: payload.fbaWarehouseCode, lineIds: payload.lineIds, details: payload.details, idempotencyKey: payload.idempotencyKey })
pendingAppointment.value = null
appointmentOpen.value = false; selectedRows.value = []; ElMessage.success('FBA 预约保存成功'); await load()
} catch (caught) { mutationError.value = getApiErrorDetails(caught, 'FBA 预约保存失败,请重试'); if (['400','403','404','409','A0400','A0403','A0404','A0409'].includes(mutationError.value.code || '')) pendingAppointment.value = null }
finally { saving.value = false }
}
function dateRange() {
const range = Array.isArray(filters.value.inboundDate) ? filters.value.inboundDate : []
return { createdFrom: range[0] ? `${String(range[0]).slice(0, 10)}T00:00:00` : undefined, createdTo: range[1] ? `${String(range[1]).slice(0, 10)}T23:59:59` : undefined }
}
async function load(model: QueryModel = filters.value) {
Object.assign(filters.value, model)
selectedRows.value = []
loading.value = true; loadError.value = null
try {
const result = await listInboundOrders({ page: page.value, size: pageSize.value, planNo: String(filters.value.orderNo || '').trim() || undefined, inboundCode: String(filters.value.inboundCode || '').trim() || undefined, lxCode: String(filters.value.lxCode || '').trim() || undefined, status: String(filters.value.status || '').trim() || undefined, warehouse: String(filters.value.warehouse || '').trim() || undefined, fbaWarehouseCode: String(filters.value.fbaWarehouse || '').trim() || undefined, appointmentStatus: String(filters.value.appointmentStatus || '').trim() || undefined, dispatchStatus: String(filters.value.dispatchStatus || '').trim() || undefined, ...dateRange() })
rows.value = result.records.map(toListItem); total.value = result.total
} catch (error) { rows.value = []; total.value = 0; loadError.value = getApiErrorDetails(error, '入库单加载失败,请重试') }
finally { loading.value = false }
}
function search(model: QueryModel) { page.value = 1; void load(model) }
function reset(model: QueryModel) { page.value = 1; void load(model) }
function openDetail(source: object) { const row = source as InboundOrderListItem; void router.push({ name: 'overseas-inbound-order-detail', params: { orderId: row.orderNo }, query: { customerId: row.customerId } }) }
async function openPlanEdit(source: object) { const row = source as InboundOrderListItem; planEditTarget.value = row; planEditOpen.value = true; planEditLoading.value = true; mutationError.value = null; try { Object.assign(planEditForm, { warehouseId: row.warehouseId, warehouseCode: row.destinationWarehouseCode === '—' ? '' : row.destinationWarehouseCode, warehouseName: row.destinationWarehouseName === '—' ? '' : row.destinationWarehouseName, version: row.version }); if (!warehouses.value.length) warehouses.value = (await listReceivingOverseasWarehouses()).records } catch (error) { planEditOpen.value = false; mutationError.value = getApiErrorDetails(error, '海外仓列表加载失败,请重试') } finally { planEditLoading.value = false } }
async function submitPlanEdit() { if (planEditSaving.value || planEditLoading.value || !access.can(permission.inboundOrderEdit)) return; const target = planEditTarget.value; if (!target || !planEditForm.warehouseId) { ElMessage.warning('请选择目的仓'); return } planEditSaving.value = true; mutationError.value = null; try { await updateInboundPlan(target.orderNo, { customerId: target.customerId, overseasWarehouseId: planEditForm.warehouseId, warehouseCode: planEditForm.warehouseCode, warehouseName: planEditForm.warehouseName, version: planEditForm.version }); planEditOpen.value = false; ElMessage.success('入库单已更新'); await load() } catch (error) { mutationError.value = getApiErrorDetails(error, '入库单更新失败,请重试') } finally { planEditSaving.value = false } }
onMounted(() => { void load() })
</script>
<template>
<AdminPage class="admin-density admin-workbench"><OperationErrorToast :details="mutationError" /><ListPageLayout aria-label="海外仓入库单列表">
<section><div class="workbench-table is-page-surface"><DataTable appearance="integrated" column-settings-mode="show" :results-visible="!loadError" v-model:page="page" v-model:page-size="pageSize" :data="rows" :columns="columns" :loading="loading" :total="total" preference-key="overseas-inbound-orders" row-key="lineId" empty-text="暂无入库单" @selection-change="onSelectionChange" @update:page="load()" @update:page-size="page = 1; load()">
<template #toolbar-start><h1>入库单</h1></template>
<template #filters><QueryToolbar v-model="filters" :fields="fields" :loading="loading" ariaLabel="入库单筛选" preference-key="overseas-inbound-orders-query" :search-on-reset="false" @search="search" @reset="reset" /></template>
<template #notices><template v-if="loadError"><LoadErrorState title="入库单加载失败" :message="loadError.message" :loading="loading" @retry="load"><template #details><ApiErrorMeta :details="loadError" /></template></LoadErrorState></template></template>
<template #toolbar-actions><el-button v-if="access.can(permission.inboundOrderAppointment)" plain :disabled="!selectedRows.length" @click="openAppointment"><CalendarCheck2 :size="16" />预约</el-button><el-button v-if="canCreateDispatch" type="primary" @click="openSmartDispatch"><Truck :size="16" />智能排车</el-button></template>
<template #actions="{ row }"><div class="row-actions density-actions admin-text-actions"><el-button class="density-action" data-tone="neutral" size="small" @click="openDetail(row)">查看</el-button><el-button class="density-action" data-tone="neutral" v-if="access.can(permission.inboundOrderEdit) && ['待入库'].includes((row as InboundOrderListItem).status)" size="small" @click="openPlanEdit(row)">编辑</el-button><el-button class="density-action" data-tone="neutral" v-if="(row as InboundOrderListItem).appointmentStatus !== '多批预约' && (row as InboundOrderListItem).appointmentNo !== '—' && access.can(permission.inboundOrderList)" size="small" @click="openAppointmentEdit(row, true)">查看预约</el-button><el-button class="density-action" data-tone="neutral" v-if="(row as InboundOrderListItem).appointmentStatus !== '多批预约' && (row as InboundOrderListItem).appointmentNo !== '—' && access.can(permission.inboundOrderAppointment)" size="small" @click="openAppointmentEdit(row)">预约</el-button><el-button v-if="(row as InboundOrderListItem).appointmentStatus === '多批预约' && access.can('platform:appointment:list')" class="density-action" @click="openMultipleAppointments(row as InboundOrderListItem)">管理多批预约</el-button></div></template>
<template #pagination-left>共 {{ total }} 条入库单 · 已选 {{ selectedRows.length }} 条</template>
</DataTable></div></section>
</ListPageLayout>
<AppDialog class="admin-density-dialog admin-workbench is-dialog" :model-value="appointmentOpen" @update:model-value="closeAppointment" :before-close="beforeCloseAppointment" :title="appointmentTitle" :show-confirm="!appointmentReadOnly" width="min(1080px, calc(100vw - 48px))" fullscreenable :loading="appointmentLoading" :confirm-disabled="appointmentLoading" :confirm-text="pendingAppointment ? '重试本次保存' : '保存预约安排'" :confirm-loading="saving" @confirm="submitAppointment">
<div class="density-dialog-sections">
<section class="density-dialog-section" aria-label="预约范围核对"><h3 class="density-dialog-section__title">预约范围</h3><table class="density-table"><tbody><tr><th scope="row">已选择</th><td>{{ appointmentSelectedCount }} 条</td><th scope="row">FBA仓库</th><td>{{ appointmentFbaWarehouse }}</td></tr></tbody></table></section>
<el-alert v-if="appointmentReplacementNos.length" :title="`原批次已被替代,当前只读。替代预约:${appointmentReplacementNos.join('、')}`" type="warning" :closable="false" show-icon />
<LoadErrorState v-if="mutationError" compact title="预约保存失败" :message="mutationError.message" :loading="saving" :show-retry="!appointmentReadOnly" :retry-text="pendingAppointment ? '重试本次保存' : '重新提交'" @retry="submitAppointment"><template #details><ApiErrorMeta :details="mutationError" /></template></LoadErrorState>
<FbaAppointmentFields continuous v-if="appointmentOpen && !appointmentLoading" ref="appointmentFields" :sources="appointmentSources" @confirmation-ready="confirmationReady = $event" :model="appointmentForm" :appointment-no="editingAppointmentNo || undefined" :timezone="appointmentTimezone" :disabled="appointmentReadOnly || appointmentLoading || saving || !!pendingAppointment" />
</div>
</AppDialog>
<SmartDispatchDialog v-model="smartDispatchOpen" :orders="selectedRows" @created="selectedRows = []; load()" />
<AppDialog class="admin-density-dialog admin-workbench is-dialog" v-model="planEditOpen" :title="`编辑入库单 · ${planEditTarget?.orderNo || '—'}`" fullscreenable confirm-text="保存" :loading="planEditLoading" :confirm-disabled="planEditLoading || !planEditForm.warehouseId" :confirm-loading="planEditSaving" @confirm="submitPlanEdit"><div class="density-dialog-sections">
<section class="density-dialog-section" data-tone="source" aria-label="入库计划来源"><h3 class="density-dialog-section__title">计划信息</h3><table class="density-table"><tbody><tr><th scope="row">入库单号</th><td>{{ planEditTarget?.orderNo || '—' }}</td></tr></tbody></table></section>
<LoadErrorState v-if="mutationError" compact title="入库单保存失败" :message="mutationError.message" :loading="planEditSaving" retry-text="重试保存" @retry="submitPlanEdit"><template #details><ApiErrorMeta :details="mutationError" /></template></LoadErrorState>
<section class="density-dialog-section" aria-label="目的仓登记"><h3 class="density-dialog-section__title">目的仓登记</h3><el-form class="density-form" label-position="left" label-width="100px" :disabled="planEditLoading || planEditSaving"><table class="density-table density-table--editable"><tbody><tr><td><el-form-item label="目的仓" required><el-select v-model="planEditForm.warehouseId" filterable placeholder="请选择目的仓" @change="(id: string) => { const w = warehouses.find((item) => item.id === id); planEditForm.warehouseCode = w?.warehouseCode || ''; planEditForm.warehouseName = w?.warehouseName || '' }"><el-option v-for="warehouse in warehouses" :key="warehouse.id" :label="`${warehouse.warehouseName}(${warehouse.warehouseCode})`" :value="warehouse.id" /></el-select></el-form-item></td></tr></tbody></table></el-form></section>
</div></AppDialog>
</AdminPage>
</template>
<style scoped>
.row-actions { min-width: max-content; flex-wrap: nowrap; white-space: nowrap; }
.row-actions :deep(.el-button) { flex: 0 0 auto; }
</style>