278 lines
27 KiB
Vue
278 lines
27 KiB
Vue
<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>
|