feat(appointment): integrate work calendar and original business actions

This commit is contained in:
2026-10-15 17:11:03 +08:00
parent af2e1481a6
commit b769d2479c
59 changed files with 2604 additions and 941 deletions
@@ -42,7 +42,7 @@ const fields = computed<QueryField[]>(() => [
{ 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' }] },
{ 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' }] },
{ 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 },
])
@@ -59,6 +59,7 @@ const appointmentForm = reactive(createAppointmentForm())
const appointmentTimezone = ref<string | 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' ? '编辑预约' : '预约')
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[]>([])
@@ -103,7 +104,7 @@ function receivingWarehouse(code: unknown, name: unknown) {
}
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: '已取消' } as Record<string, string>)[String(value)] || text(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 {
@@ -122,6 +123,7 @@ function toListItem(dto: InboundOrderSummaryDto): InboundOrderListItem {
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 仓库'
@@ -131,6 +133,7 @@ function selectionError() {
function openAppointment() {
if (!access.can(permission.inboundOrderAppointment) || !canLeaveAppointment()) return
appointmentViewOnly.value = false
appointmentReplacementNos.value = []
const error = selectionError()
if (error) { ElMessage.warning(error); return }
appointmentMode.value = 'create'; editingAppointmentNo.value = ''
@@ -148,12 +151,15 @@ function openSmartDispatch() {
async function openAppointmentEdit(source: object, readOnly = false) {
if (!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
@@ -163,6 +169,11 @@ async function openAppointmentEdit(source: object, readOnly = false) {
mutationError.value = getApiErrorDetails(caught, 'FBA 预约详情加载失败,请重试')
} finally { appointmentLoading.value = false }
}
function openMultipleAppointments(row: InboundOrderListItem) {
if (!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}` } })
}
function canLeaveAppointment() {
if (saving.value || pendingAppointment.value) { ElMessage.warning('请先核实本次预约保存结果,再关闭或切换'); return false }
@@ -224,14 +235,14 @@ onMounted(() => { void load() })
<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="access.can(permission.inboundOrderSmartDispatch)" 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).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).appointmentNo !== '—' && access.can(permission.inboundOrderAppointment)" size="small" @click="openAppointmentEdit(row)">预约</el-button></div></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></section>
</ListPageLayout>
<AppDialog class="admin-density-dialog" :model-value="appointmentOpen" @update:model-value="closeAppointment" :before-close="beforeCloseAppointment" :title="appointmentTitle" :show-confirm="!appointmentReadOnly" width="min(1080px, calc(100vw - 48px))" :confirm-text="pendingAppointment ? '重试本次保存' : '保存预约安排'" :confirm-loading="saving" @confirm="submitAppointment">
<div v-loading="appointmentLoading">
<div class="appointment-summary">已选择 {{ appointmentSelectedCount }} 条,FBA仓库:{{ appointmentFbaWarehouse }}</div>
<FbaAppointmentFields :model="appointmentForm" :appointment-no="editingAppointmentNo || undefined" :timezone="appointmentTimezone" :disabled="appointmentReadOnly || appointmentLoading || saving || !!pendingAppointment" />
<p v-if="appointmentReplacementNos.length">原批次已被替代,当前只读。替代预约:{{ appointmentReplacementNos.join('、') }}</p><FbaAppointmentFields :model="appointmentForm" :appointment-no="editingAppointmentNo || undefined" :timezone="appointmentTimezone" :disabled="appointmentReadOnly || appointmentLoading || saving || !!pendingAppointment" />
</div>
</AppDialog>