Files
admin-web/src/modules/overseas-warehouse/pages/DeliveryOrdersView.vue
T
2026-08-26 15:48:34 +08:00

79 lines
15 KiB
Vue

<script setup lang="ts">
import { onMounted, reactive, ref } from 'vue'
import { Eye, Pencil, RotateCcw } from '@lucide/vue'
import { ElMessage } from 'element-plus'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import { useRouter } from 'vue-router'
import AdminPage from '@/shared/components/AdminPage.vue'
import AppDialog from '@/shared/components/AppDialog.vue'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.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 QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
import StatusBadge from '@/shared/components/StatusBadge.vue'
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
import { formatOverseasDateTime } from '../lastMilePresentation'
import { listDeliveryOrders, listDeliveryReturnInboundOptions, markDeliveryReturn, type DeliveryOrderRow } from '../api/deliveryOrders'
import { updateDeliveryOrderStatus } from '../api/customerVisits'
import type { ExceptionRelation } from '../api/exceptionOrders'
import { overseasWarehousePermissions as permission } from '../permissions'
const router = useRouter()
const access = useAccessPreviewStore()
const rows = ref<DeliveryOrderRow[]>([]); const page = ref(1); const pageSize = ref(10); const total = ref(0); const loading = ref(false); const error = ref<ApiErrorDetails | null>(null)
const filters = ref<QueryModel>({ documentNo: '', dispatchNo: '', waybillNo: '', warehouseId: '', status: '', vehiclePlate: '', driverName: '', deliveryMethod: '', plannedAt: [] })
const fields: QueryField[] = [
{ key: 'status', type: 'select', label: '状态', placeholder: '全部状态', options: [{ label: '待派送', value: 'WAIT_DELIVERY' }, { label: '派送中', value: 'DELIVERING' }, { label: '派送完成', value: 'DELIVERED' }, { label: '已签收(历史)', value: 'SIGNED' }, { label: '已拒签', value: 'REJECTED' }, { label: '退货处理中', value: 'RETURN_PENDING' }, { label: '已取消', value: 'CANCELLED' }] },
{ key: 'documentNo', type: 'input', label: '派送单号', placeholder: '请输入派送单号' },
{ key: 'dispatchNo', type: 'input', label: '排车单号', placeholder: '请输入排车单号' },
{ key: 'waybillNo', type: 'input', label: '运单号', placeholder: '请输入运单号' },
{ key: 'warehouseId', type: 'input', label: '仓库', placeholder: '请输入仓库ID' },
{ key: 'deliveryMethod', type: 'select', label: '派送方式', placeholder: '全部方式', options: [{ label: 'FBA自提', value: 'FBA_PICKUP' }, { label: '客户自提', value: 'CUSTOMER_PICKUP' }, { label: '卡派打托', value: 'TRUCK_PALLET' }, { label: '卡派地板', value: 'TRUCK_FLOOR' }, { label: '快递', value: 'EXPRESS' }, { label: '外配', value: 'OUTSOURCED' }, { label: '专车直送', value: 'DEDICATED_TRUCK' }, { label: '零担派送', value: 'LTL' }] },
{ key: 'vehiclePlate', type: 'input', label: '车牌号', placeholder: '请输入车牌号' },
{ key: 'driverName', type: 'input', label: '司机', placeholder: '请输入司机或车队' },
{ key: 'plannedAt', type: 'daterange', label: '预计派送时间' },
]
const columns: DataTableColumn[] = [
{ key: 'actions', label: '操作', width: 300, fixed: 'left', slotName: 'actions' },
{ key: 'deliveryNo', prop: 'deliveryNo', label: '派送单号', minWidth: 180, fixed: 'left' },
{ key: 'customerName', prop: 'customerName', label: '客户', minWidth: 160 },
{ key: 'overseasWarehouseName', prop: 'overseasWarehouseName', label: '仓库', minWidth: 150 },
{ key: 'dispatchNo', prop: 'dispatchNo', label: '排车单号', minWidth: 170 },
{ key: 'waybillNo', prop: 'waybillNo', label: '运单号', minWidth: 150 },
{ key: 'status', prop: 'status', label: '订单状态', width: 110, slotName: 'status' },
{ key: 'deliveryMethod', prop: 'deliveryMethod', label: '派送方式', width: 120, formatter: (row: DeliveryOrderRow) => deliveryMethodLabel(row.deliveryMethod) },
{ key: 'carrierName', prop: 'carrierName', label: '派送人', minWidth: 150 },
{ key: 'vehiclePlate', prop: 'vehiclePlate', label: '车牌', minWidth: 120 },
{ key: 'driverName', prop: 'driverName', label: '司机/车队', minWidth: 140 },
{ key: 'plannedDeliveryAt', prop: 'plannedDeliveryAt', label: '期望派送时间', minWidth: 170, formatter: (row: DeliveryOrderRow) => formatOverseasDateTime(row.plannedDeliveryAt) },
{ key: 'dispatchedAt', prop: 'dispatchedAt', label: '派送时间', minWidth: 170, formatter: (row: DeliveryOrderRow) => formatOverseasDateTime(row.dispatchedAt) },
{ key: 'pickupMethod', prop: 'pickupMethod', label: '提货方式', width: 110 },
{ key: 'loadingType', prop: 'loadingType', label: '装车类型', width: 110 },
{ key: 'packageCount', prop: 'packageCount', label: '总箱数', width: 90, align: 'right' },
{ key: 'totalWeightKg', prop: 'totalWeightKg', label: '总重量(kg)', width: 120, align: 'right' },
{ key: 'totalVolumeCbm', prop: 'totalVolumeCbm', label: '总体积(m³)', width: 120, align: 'right' },
{ key: 'expectedArrivalAt', prop: 'expectedArrivalAt', label: '预计到达时间', minWidth: 170, formatter: (row: DeliveryOrderRow) => formatOverseasDateTime(row.expectedArrivalAt) },
{ key: 'signedAt', prop: 'signedAt', label: '签收时间', minWidth: 170, formatter: (row: DeliveryOrderRow) => formatOverseasDateTime(row.signedAt) },
{ key: 'remark', prop: 'remark', label: '备注', minWidth: 160 },
]
const statusLabels: Record<string, string> = { WAIT_DELIVERY: '待派送', DELIVERING: '派送中', DELIVERED: '派送完成', SIGNED: '已签收', REJECTED: '已拒签', RETURN_PENDING: '退货处理中', CANCELLED: '已取消' }
function deliveryMethodLabel(value?: string | null) { return ({ FBA_PICKUP: 'FBA自提', CUSTOMER_PICKUP: '客户自提', TRUCK_PALLET: '卡派打托', TRUCK_FLOOR: '卡派地板', EXPRESS: '快递', OUTSOURCED: '外配', DEDICATED_TRUCK: '专车直送', LTL: '零担派送', SELF_DELIVERY: '自送', THIRD_PARTY: '第三方配送' } as Record<string, string>)[String(value)] || value || '—' }
async function load(model: QueryModel = filters.value) { Object.assign(filters.value, model); loading.value = true; error.value = null; try { const range = Array.isArray(filters.value.plannedAt) ? filters.value.plannedAt.map(String) : []; const result = await listDeliveryOrders({ page: page.value, size: pageSize.value, documentNo: text('documentNo'), dispatchNo: text('dispatchNo'), waybillNo: text('waybillNo'), warehouseId: text('warehouseId'), status: text('status'), vehiclePlate: text('vehiclePlate'), driverName: text('driverName'), deliveryMethod: text('deliveryMethod'), plannedFrom: range[0] ? `${range[0]}T00:00:00` : undefined, plannedTo: range[1] ? `${range[1]}T23:59:59` : undefined }); rows.value = result.records; total.value = result.total } catch (cause) { rows.value = []; total.value = 0; error.value = getApiErrorDetails(cause, '派送单列表加载失败') } finally { loading.value = false } }
function text(key: string) { return String(filters.value[key] || '').trim() || undefined }
function open(row: DeliveryOrderRow) { void router.push({ name: 'overseas-delivery-order-detail', params: { deliveryNo: row.deliveryNo }, query: { customerId: row.customerId } }) }
const returnOpen = ref(false); const returnLoading = ref(false); const returnSaving = ref(false); const returnRow = ref<DeliveryOrderRow | null>(null); const returnOptions = ref<ExceptionRelation[]>([])
const returnForm = reactive({ inboundOrderId: '', reason: '', returnDirection: 'OVERSEAS_WAREHOUSE', returnLogisticsNo: '', packageCount: undefined as number | undefined, skuCode: '' })
async function openReturn(row: DeliveryOrderRow) { returnRow.value = row; Object.assign(returnForm, { inboundOrderId: '', reason: '', returnDirection: 'OVERSEAS_WAREHOUSE', returnLogisticsNo: '', packageCount: undefined, skuCode: '' }); returnOptions.value = []; returnOpen.value = true; returnLoading.value = true; try { returnOptions.value = await listDeliveryReturnInboundOptions(row.deliveryNo, row.customerId); if (returnOptions.value.length === 1) returnForm.inboundOrderId = String(returnOptions.value[0].inboundOrderId || '') } catch (cause) { ElMessage.error(getApiErrorDetails(cause, '退货入仓单加载失败').message) } finally { returnLoading.value = false } }
async function submitReturn() { if (!returnRow.value || !returnForm.inboundOrderId || !returnForm.reason.trim()) { ElMessage.warning('请选择入仓单并填写退货原因'); return }; returnSaving.value = true; try { const result = await markDeliveryReturn(returnRow.value.deliveryNo, returnRow.value.customerId, { inboundOrderId: returnForm.inboundOrderId, reason: returnForm.reason.trim(), returnDirection: returnForm.returnDirection, returnLogisticsNo: returnForm.returnLogisticsNo.trim() || undefined, packageCount: returnForm.packageCount, skuCode: returnForm.skuCode.trim() || undefined }); returnOpen.value = false; ElMessage.success(`已生成异常退货单 ${result.exceptionNo}`); await load() } catch (cause) { ElMessage.error(getApiErrorDetails(cause, '标记退货失败').message) } finally { returnSaving.value = false } }
const statusOpen = ref(false); const statusSaving = ref(false); const statusRow = ref<DeliveryOrderRow | null>(null); const statusForm = reactive({ status: '', remark: '' })
function openStatus(row: DeliveryOrderRow) { statusRow.value = row; Object.assign(statusForm, { status: row.status, remark: '' }); statusOpen.value = true }
async function submitStatus() { if (!statusRow.value || !statusForm.status) return; statusSaving.value = true; try { const result = await updateDeliveryOrderStatus(statusRow.value.deliveryNo, statusRow.value.customerId, { status: statusForm.status, remark: statusForm.remark.trim() || undefined }); statusOpen.value = false; ElMessage.success(result.deliveryStatus === 'DELIVERED' ? '派送已完成,待回访记录已生成' : '派送状态已更新'); await load() } catch (cause) { ElMessage.error(getApiErrorDetails(cause, '派送状态更新失败').message) } finally { statusSaving.value = false } }
function search(model: QueryModel) { page.value = 1; void load(model) }
function reset(model: QueryModel) { page.value = 1; void load(model) }
onMounted(load)
</script>
<template><AdminPage><ListPageLayout aria-label="派送单列表"><template #toolbar><QueryToolbar v-model="filters" class="surface-panel" :fields="fields" :loading="loading" ariaLabel="派送单筛选" preference-key="overseas-delivery-orders-query" @search="search" @reset="reset" /></template><template v-if="error" #state><LoadErrorState title="派送单列表加载失败" :message="error.message" :loading="loading" @retry="load"><template #details><ApiErrorMeta :details="error" /></template></LoadErrorState></template><section v-if="!error" class="surface-panel table-panel"><DataTable v-model:page="page" v-model:page-size="pageSize" :data="rows" :columns="columns" :loading="loading" :total="total" preference-key="overseas-delivery-orders" row-key="id" empty-text="暂无派送单" @update:page="load()" @update:page-size="page = 1; load()"><template #actions="{ row }"><el-button size="small" type="primary" plain @click="open(row as DeliveryOrderRow)"><Eye :size="15" />详情</el-button><el-button v-if="access.can(permission.deliveryStatus) && row.status !== 'RETURN_PENDING'" size="small" plain @click="openStatus(row as DeliveryOrderRow)"><Pencil :size="15" />编辑状态</el-button><el-button v-if="access.can(permission.deliveryReturn) && !['RETURN_PENDING','CANCELLED'].includes(String(row.status))" size="small" type="warning" plain @click="openReturn(row as DeliveryOrderRow)"><RotateCcw :size="15" />标记退货</el-button></template><template #status="{ row }"><StatusBadge :status="statusLabels[String(row.status)] || String(row.status || '—')" /></template><template #pagination-left>共 {{ total }} 条派送单</template></DataTable></section></ListPageLayout></AdminPage><AppDialog v-model="statusOpen" title="编辑派送状态" width="min(520px, calc(100vw - 28px))" confirm-text="保存状态" :confirm-loading="statusSaving" @confirm="submitStatus"><el-form label-position="top"><el-form-item label="派送单号"><el-input :model-value="statusRow?.deliveryNo" readonly /></el-form-item><el-form-item label="派送状态" required><el-select v-model="statusForm.status"><el-option label="待派送" value="WAIT_DELIVERY" /><el-option label="派送中" value="DELIVERING" /><el-option label="派送完成" value="DELIVERED" /><el-option label="已拒签" value="REJECTED" /><el-option label="已取消" value="CANCELLED" /></el-select></el-form-item><el-form-item label="备注"><el-input v-model="statusForm.remark" type="textarea" :rows="3" maxlength="500" show-word-limit /></el-form-item><el-alert v-if="statusForm.status === 'DELIVERED'" title="保存为派送完成后,将自动生成一条待回访记录。" type="info" :closable="false" /></el-form></AppDialog><AppDialog v-model="returnOpen" title="标记退货" width="min(720px, calc(100vw - 28px))" confirm-text="确认转入退货" :loading="returnLoading" :confirm-loading="returnSaving" @confirm="submitReturn"><el-form label-position="top"><el-form-item label="派送单号"><el-input :model-value="returnRow?.deliveryNo" readonly /></el-form-item><el-form-item label="对应入仓单" required><el-select v-model="returnForm.inboundOrderId" placeholder="请选择该派送单对应的入仓单"><el-option v-for="option in returnOptions" :key="String(option.inboundOrderId)" :value="String(option.inboundOrderId)" :label="`${option.inboundCode} · ${option.lastMileOrderNo || '无尾程单号'}`" /></el-select></el-form-item><el-form-item label="退货原因" required><el-input v-model="returnForm.reason" type="textarea" :rows="4" maxlength="1000" show-word-limit /></el-form-item><div class="return-grid"><el-form-item label="退货去向" required><el-select v-model="returnForm.returnDirection"><el-option label="退回海外仓" value="OVERSEAS_WAREHOUSE" /><el-option label="退回客户" value="CUSTOMER" /><el-option label="其他" value="OTHER" /></el-select></el-form-item><el-form-item label="退回物流单号"><el-input v-model="returnForm.returnLogisticsNo" placeholder="可安排物流后补充" /></el-form-item><el-form-item label="箱数"><el-input-number v-model="returnForm.packageCount" :min="1" /></el-form-item><el-form-item label="SKU(可自由填写)"><el-input v-model="returnForm.skuCode" /></el-form-item></div><el-alert title="确认后会生成异常退货单,并将派送单标记为退货处理中。" type="warning" :closable="false" /></el-form></AppDialog></template>
<style scoped>.return-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 18px; } @media (max-width: 720px) { .return-grid { grid-template-columns: 1fr; } }</style>