42 lines
6.6 KiB
Vue
42 lines
6.6 KiB
Vue
<script setup lang="ts">
|
||
import { computed, ref, watch } from 'vue'
|
||
import { ArrowLeft, XCircle } from '@lucide/vue'
|
||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||
import { useRoute, useRouter } from 'vue-router'
|
||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||
import AdminPage from '@/shared/components/AdminPage.vue'
|
||
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
||
import DetailPageLayout from '@/shared/components/DetailPageLayout.vue'
|
||
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
|
||
import OperationErrorToast from '@/shared/components/OperationErrorToast.vue'
|
||
import StatusBadge from '@/shared/components/StatusBadge.vue'
|
||
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
||
import { cancelOutbound, getOutbound, type OutboundDetail } from '../api/outbounds'
|
||
import { formatOverseasDateTime } from '../lastMilePresentation'
|
||
import { overseasWarehousePermissions as permission } from '../permissions'
|
||
|
||
const route = useRoute(); const router = useRouter(); const access = useAccessPreviewStore()
|
||
const outboundNo = computed(() => String(route.params.outboundNo || '').trim())
|
||
const detail = ref<OutboundDetail | null>(null); const loading = ref(false); const cancelling = ref(false)
|
||
const loadError = ref<ApiErrorDetails | null>(null); const mutationError = ref<ApiErrorDetails | null>(null)
|
||
const statusLabels: Record<string, string> = { WAIT_OUTBOUND: '待出库', PICKING: '拣货中', PICKED: '已拣货', OUTBOUNDED: '已出库', CANCELLED: '已取消' }
|
||
function show(value: unknown) { const text = String(value ?? '').trim(); return text || '—' }
|
||
function statusLabel(value: unknown) { const code = String(value || ''); return statusLabels[code] || show(code) }
|
||
const canCancel = computed(() => access.can(permission.outboundCancel) && detail.value?.outbound.status === 'PICKED')
|
||
async function load() { if (!outboundNo.value) { loadError.value = { message: '详情缺少出库单号,请从出库单列表重新进入' }; return } loading.value = true; loadError.value = null; try { detail.value = await getOutbound(outboundNo.value) } catch (cause) { detail.value = null; loadError.value = getApiErrorDetails(cause, '出库单详情加载失败,请重试') } finally { loading.value = false } }
|
||
async function cancel() { if (!canCancel.value || cancelling.value) return; let reason = ''; try { const result = await ElMessageBox.prompt(`确认取消出库单 ${outboundNo.value}?`, '取消出库单', { inputType: 'textarea', inputPlaceholder: '请输入取消原因', inputPattern: /\S+/, inputErrorMessage: '请输入取消原因', confirmButtonText: '确认取消', cancelButtonText: '返回', type: 'warning' }); reason = result.value.trim() } catch { return } cancelling.value = true; mutationError.value = null; try { await cancelOutbound(outboundNo.value, reason); ElMessage.success('出库单已取消'); await load() } catch (cause) { mutationError.value = getApiErrorDetails(cause, '取消出库单失败,请重试') } finally { cancelling.value = false } }
|
||
function back() { void router.push({ name: 'overseas-outbounds' }) }
|
||
watch(outboundNo, () => { void load() }, { immediate: true })
|
||
</script>
|
||
|
||
<template>
|
||
<AdminPage><DetailPageLayout aria-label="出库单详情">
|
||
<template #header><div class="detail-heading"><el-button aria-label="返回出库单列表" @click="back"><ArrowLeft :size="16" />返回</el-button><div class="detail-title"><div><h1>出库单详情</h1><p>出库单号 {{ outboundNo }}</p></div><StatusBadge v-if="detail" :status="statusLabel(detail.outbound.status)" /></div><el-button v-if="canCancel" type="danger" plain :loading="cancelling" @click="cancel"><XCircle :size="16" />取消出库单</el-button></div></template>
|
||
<OperationErrorToast :details="mutationError" />
|
||
<LoadErrorState v-if="loadError" title="出库单详情加载失败" :message="loadError.message" :loading="loading" @retry="load"><template #details><ApiErrorMeta :details="loadError" /></template></LoadErrorState>
|
||
<template v-else-if="detail"><section class="surface-panel detail-section"><h2>出库信息</h2><el-descriptions :column="4" border><el-descriptions-item label="出库单号">{{ show(detail.outbound.outboundNo) }}</el-descriptions-item><el-descriptions-item label="关联排车单">{{ show(detail.outbound.dispatchNo) }}</el-descriptions-item><el-descriptions-item label="状态">{{ statusLabel(detail.outbound.status) }}</el-descriptions-item><el-descriptions-item label="海外仓">{{ show(detail.outbound.overseasWarehouseId) }}</el-descriptions-item><el-descriptions-item label="托盘数">{{ detail.outbound.palletCount ?? '—' }}</el-descriptions-item><el-descriptions-item label="箱数">{{ detail.outbound.packageCount ?? '—' }}</el-descriptions-item><el-descriptions-item label="创建时间">{{ formatOverseasDateTime(detail.outbound.createdAt) }}</el-descriptions-item></el-descriptions></section><section class="surface-panel detail-section"><h2>关联派送</h2><el-empty v-if="!detail.delivery" description="暂无关联派送单" :image-size="56" /><el-descriptions v-else :column="4" border><el-descriptions-item label="派送单号">{{ show(detail.delivery.deliveryNo) }}</el-descriptions-item><el-descriptions-item label="派送状态">{{ show(detail.delivery.status) }}</el-descriptions-item><el-descriptions-item label="运单号">{{ show(detail.delivery.waybillNo) }}</el-descriptions-item><el-descriptions-item label="承运商">{{ show(detail.delivery.carrierName) }}</el-descriptions-item><el-descriptions-item label="司机">{{ show(detail.delivery.driverName) }}</el-descriptions-item><el-descriptions-item label="车牌">{{ show(detail.delivery.vehiclePlate) }}</el-descriptions-item><el-descriptions-item label="计划派送">{{ formatOverseasDateTime(detail.delivery.plannedDeliveryAt) }}</el-descriptions-item><el-descriptions-item label="备注">{{ show(detail.delivery.remark) }}</el-descriptions-item></el-descriptions></section></template>
|
||
</DetailPageLayout></AdminPage>
|
||
</template>
|
||
|
||
<style scoped>.detail-heading{display:flex;min-width:0;align-items:center;gap:14px}.detail-title{display:flex;min-width:0;flex:1;align-items:center;justify-content:space-between;gap:12px}.detail-title h1{margin:0;font-size:20px}.detail-title p{margin:3px 0 0;color:var(--cc-muted);font-size:12px}.detail-section{padding:16px}.detail-section h2{margin:0 0 14px;font-size:16px}.detail-section+.detail-section{margin-top:16px}@media(max-width:1200px){.detail-section :deep(.el-descriptions__body .el-descriptions__table){table-layout:auto}}@media(max-width:720px){.detail-heading,.detail-title{align-items:flex-start;flex-direction:column}}</style>
|