优化
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch, type Component } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { ArrowLeft, Box, ChevronDown, ChevronUp, CircleCheck, Eye, GitBranch, History, Link2, ListChecks, MoreHorizontal, PackageOpen, Ruler, SlidersHorizontal, StickyNote, WalletCards } from '@lucide/vue'
|
||||
import { ArrowLeft, Box, ChevronDown, ChevronUp, CircleCheck, Download, Eye, GitBranch, History, Link2, ListChecks, MoreHorizontal, PackageOpen, Ruler, SlidersHorizontal, StickyNote, WalletCards } from '@lucide/vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import {
|
||||
getPlatformUnifiedOrder,
|
||||
@@ -22,8 +22,10 @@ import InboundFeeDetailManager from '@/modules/unified-orders/components/Inbound
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam'
|
||||
import { listAllPlatformWarehouses, type PlatformWarehouseSummaryResource } from '@/modules/warehouses'
|
||||
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
||||
import { getLoadingAssociationSummary, getOverseasInboundCodeRelationship, type LoadingAssociationSummary, type OverseasInboundCodeRelationship } from '@/shared/api/inboundCodeRelations'
|
||||
import { getApiErrorDetails, getApiErrorMessage, type ApiErrorDetails } from '@/shared/api/http'
|
||||
import { createPlatformExportTask } from '@/modules/downloads'
|
||||
import { showExportTaskCreatedDialog } from '@/shared/components/exportTaskCreatedDialog'
|
||||
import { getLoadingAssociationSummary, getOverseasInboundCodeRelationship, getPlatformInboundCodeAssociation, type LoadingAssociationSummary, type OverseasInboundCodeRelationship } from '@/shared/api/inboundCodeRelations'
|
||||
import AdminPage from '@/shared/components/AdminPage.vue'
|
||||
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
||||
import AppDialog from '@/shared/components/AppDialog.vue'
|
||||
@@ -123,6 +125,7 @@ const approvalOpen = ref(false)
|
||||
const approvalLoadingId = ref('')
|
||||
const approvalSaving = ref(false)
|
||||
const batchApprovalSaving = ref(false)
|
||||
const exportSubmitting = ref(false)
|
||||
const selectedApprovalOrders = ref<UnifiedOrderResource[]>([])
|
||||
const selectedOrders = ref<UnifiedOrderResource[]>([])
|
||||
const coLoadCreateOpen = ref(false)
|
||||
@@ -161,8 +164,10 @@ const overseasRelationship = ref<OverseasInboundCodeRelationship | null>(null)
|
||||
const loadingAssociationError = ref<ApiErrorDetails | null>(null)
|
||||
const overseasRelationshipError = ref<ApiErrorDetails | null>(null)
|
||||
const subsequentFulfillmentLoading = ref(false)
|
||||
const resolvedDetailTarget = ref<{ orderId: string; tenantId: string } | null>(null)
|
||||
let detailLoadingKey = ''
|
||||
let detailRequestSerial = 0
|
||||
let detailAssociationRequestSerial = 0
|
||||
let measurementRequestSerial = 0
|
||||
let measurementPreviewRequestSerial = 0
|
||||
let subsequentFulfillmentRequestSerial = 0
|
||||
@@ -170,9 +175,15 @@ const showAdvancedFilters = ref(false)
|
||||
const filters = ref<FilterModel>(emptyFilters())
|
||||
const appliedFilters = ref<FilterModel>(emptyFilters())
|
||||
const selectedStatus = ref<UnifiedOrderMainStatus | ''>('')
|
||||
const orderId = computed(() => typeof route.params.orderId === 'string' ? route.params.orderId : '')
|
||||
const detailTenantId = computed(() => typeof route.query.tenantId === 'string' ? route.query.tenantId : '')
|
||||
const businessInboundCode = computed(() => route.name === 'unified-order-detail-by-inbound-code' && typeof route.params.inboundCode === 'string'
|
||||
? route.params.inboundCode.trim()
|
||||
: '')
|
||||
const orderId = computed(() => typeof route.params.orderId === 'string' ? route.params.orderId : resolvedDetailTarget.value?.orderId || '')
|
||||
const detailTenantId = computed(() => typeof route.query.tenantId === 'string' ? route.query.tenantId : resolvedDetailTarget.value?.tenantId || '')
|
||||
const detailRouteKey = computed(() => orderId.value ? `${detailTenantId.value}:${orderId.value}` : '')
|
||||
const isDetailRoute = computed(() => Boolean(orderId.value || businessInboundCode.value))
|
||||
const requestedDetailTab = computed(() => route.query.tab === 'measurement' ? 'measurement' : 'basic')
|
||||
const fromMeasurementReview = computed(() => route.query.from === 'measurement-review')
|
||||
function normalizedIdentifier(value: unknown) {
|
||||
const identifier = String(value ?? '').trim()
|
||||
return identifier && identifier !== 'null' && identifier !== 'undefined' ? identifier : ''
|
||||
@@ -261,9 +272,9 @@ const queryFields = computed(() => showAdvancedFilters.value
|
||||
|
||||
const columns = computed<DataTableColumn[]>(() => [
|
||||
{ key: 'actions', label: '操作', width: 300, fixed: 'left', slotName: 'actions' },
|
||||
...(can('platform:order:approve') || can('platform:firstleg:smart-loading:assign') ? [{
|
||||
...(can('platform:order:approve') || can('platform:firstleg:smart-loading:assign') || can('platform:order:export') ? [{
|
||||
key: 'selection', type: 'selection', width: 48, fixed: 'left',
|
||||
selectable: () => !batchApprovalSaving.value && !coLoadSaving.value,
|
||||
selectable: () => !batchApprovalSaving.value && !coLoadSaving.value && !exportSubmitting.value,
|
||||
} satisfies DataTableColumn] : []),
|
||||
{ key: 'inboundCode', prop: 'inboundCode', label: '入仓码', minWidth: 260, slotName: 'inboundCode' },
|
||||
{ key: 'mainStatus', prop: 'mainStatus', label: '订单主状态', minWidth: 130, slotName: 'mainStatus' },
|
||||
@@ -282,7 +293,7 @@ const columns = computed<DataTableColumn[]>(() => [
|
||||
{ key: 'chargeableWeightKg', prop: 'chargeableWeightKg', label: '计费毛重', minWidth: 120, slotName: 'field' },
|
||||
{ key: 'chargeableVolumeCbm', prop: 'chargeableVolumeCbm', label: '计费体积(m³)', minWidth: 130, slotName: 'field' },
|
||||
{ key: 'expectedShipDate', prop: 'expectedShipDate', label: '预计出运日期', minWidth: 150, slotName: 'field' },
|
||||
{ key: 'expectedArrivalAt', prop: 'expectedArrivalAt', label: '预计到达时间', minWidth: 160, slotName: 'field', defaultVisible: false },
|
||||
{ key: 'expectedArrivalAt', prop: 'expectedArrivalAt', label: '预计到货时间', minWidth: 160, slotName: 'field' },
|
||||
{ key: 'feeReviewStatus', prop: 'feeReviewStatus', label: '费用审核状态', minWidth: 140, slotName: 'field', defaultVisible: false },
|
||||
{ key: 'skuNos', prop: 'skuNos', label: 'SKU 编码', minWidth: 180, slotName: 'field', defaultVisible: false },
|
||||
{ key: 'totalQuantity', prop: 'totalQuantity', label: '货物总数量', minWidth: 120, slotName: 'field', defaultVisible: false },
|
||||
@@ -1039,6 +1050,88 @@ function closeDetail() {
|
||||
void router.push({ name: 'unified-orders' })
|
||||
}
|
||||
|
||||
function returnToMeasurementReviews() {
|
||||
if (window.history.length > 1) router.back()
|
||||
else void router.push({ name: 'inbound-measurement-reviews' })
|
||||
}
|
||||
|
||||
async function resolveBusinessDetailTarget() {
|
||||
const inboundCode = businessInboundCode.value
|
||||
const serial = ++detailAssociationRequestSerial
|
||||
resolvedDetailTarget.value = null
|
||||
detail.value = null
|
||||
detailError.value = null
|
||||
if (!inboundCode) {
|
||||
detailError.value = { message: '入仓码缺失,无法定位对应订单' }
|
||||
return
|
||||
}
|
||||
detailLoading.value = true
|
||||
try {
|
||||
const association = await getPlatformInboundCodeAssociation(inboundCode)
|
||||
if (serial !== detailAssociationRequestSerial || inboundCode !== businessInboundCode.value) return
|
||||
if (!association.orderId || !association.customerId) {
|
||||
detailError.value = { message: `未找到入仓码 ${inboundCode} 对应的订单,请返回测量审核列表重新打开` }
|
||||
return
|
||||
}
|
||||
resolvedDetailTarget.value = { orderId: String(association.orderId), tenantId: String(association.customerId) }
|
||||
} catch (error) {
|
||||
if (serial !== detailAssociationRequestSerial || inboundCode !== businessInboundCode.value) return
|
||||
detailError.value = getApiErrorDetails(error, `入仓码 ${inboundCode} 的订单关联加载失败,可能无权查看或目标已不存在`)
|
||||
} finally {
|
||||
if (serial === detailAssociationRequestSerial && !resolvedDetailTarget.value) detailLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function exportFilterParameters() {
|
||||
const activeFilters = appliedFilters.value
|
||||
const range = activeFilters.createdRange
|
||||
return {
|
||||
tenantId: activeFilters.tenantId || undefined,
|
||||
mainStatus: selectedStatus.value || undefined,
|
||||
receivingWarehouseId: activeFilters.receivingWarehouseId || undefined,
|
||||
destinationWarehouseId: activeFilters.destinationWarehouseId || undefined,
|
||||
inboundCode: trimmed(activeFilters.inboundCode),
|
||||
expectedArrivalDate: activeFilters.expectedArrivalDate || undefined,
|
||||
feeReviewStatus: activeFilters.feeReviewStatus || undefined,
|
||||
channelName: trimmed(activeFilters.channelName),
|
||||
containerNo: trimmed(activeFilters.containerNo),
|
||||
cabinetNo: trimmed(activeFilters.cabinetNo),
|
||||
sailingDate: activeFilters.sailingDate || undefined,
|
||||
loaded: optionalBoolean(activeFilters.loaded),
|
||||
fullContainer: optionalBoolean(activeFilters.fullContainer),
|
||||
measurementReviewStatus: activeFilters.measurementReviewStatus || undefined,
|
||||
fbaCode: trimmed(activeFilters.fbaCode),
|
||||
createdFrom: range.length === 2 ? `${range[0]}T00:00:00` : undefined,
|
||||
createdTo: range.length === 2 ? `${range[1]}T23:59:59` : undefined,
|
||||
}
|
||||
}
|
||||
|
||||
async function exportOrders() {
|
||||
if (exportSubmitting.value) return
|
||||
const selectedOrderNos = selectedOrders.value.map((row) => row.orderNo?.trim() || '')
|
||||
if (selectedOrderNos.some((orderNo) => !orderNo)) {
|
||||
ElMessage.error('所选订单缺少订单号,请刷新列表后重试')
|
||||
return
|
||||
}
|
||||
exportSubmitting.value = true
|
||||
try {
|
||||
const task = await createPlatformExportTask({
|
||||
exportType: 'DOMESTIC_WAREHOUSE_ORDER_LIST',
|
||||
format: 'XLSX',
|
||||
taskName: selectedOrderNos.length ? `国内仓订单-已选${selectedOrderNos.length}票` : '国内仓订单-当前筛选',
|
||||
parameters: selectedOrderNos.length
|
||||
? { selectedOrderNos }
|
||||
: exportFilterParameters(),
|
||||
})
|
||||
const openDownloadCenter = await showExportTaskCreatedDialog({ taskName: task.taskName, taskNo: task.taskNo })
|
||||
if (openDownloadCenter) await router.push({ name: 'download-center' })
|
||||
} catch (error) {
|
||||
ElMessage.error(getApiErrorMessage(error, '订单导出任务创建失败'))
|
||||
} finally {
|
||||
exportSubmitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadDetail(force = false) {
|
||||
if (!orderId.value) return
|
||||
const tenantId = detailTenantId.value
|
||||
@@ -1064,6 +1157,10 @@ async function loadDetail(force = false) {
|
||||
if (serial !== detailRequestSerial || requestKey !== detailRouteKey.value) return
|
||||
detail.value = response
|
||||
await loadCargoImagePreviews(response, serial)
|
||||
if (requestedDetailTab.value === 'measurement') {
|
||||
activeDetailTab.value = 'measurement'
|
||||
await loadMeasurementDetails()
|
||||
}
|
||||
} catch (error) {
|
||||
if (serial !== detailRequestSerial || requestKey !== detailRouteKey.value) return
|
||||
detail.value = null
|
||||
@@ -1075,13 +1172,18 @@ async function loadDetail(force = false) {
|
||||
}
|
||||
|
||||
watch([page, pageSize], () => { void loadOrders() })
|
||||
watch(businessInboundCode, (value) => {
|
||||
detailAssociationRequestSerial += 1
|
||||
resolvedDetailTarget.value = null
|
||||
if (value) void resolveBusinessDetailTarget()
|
||||
}, { immediate: true })
|
||||
watch(detailRouteKey, (value) => {
|
||||
if (value) {
|
||||
activeDetailTab.value = 'basic'
|
||||
activeDetailTab.value = requestedDetailTab.value
|
||||
detail.value = null
|
||||
detailError.value = null
|
||||
void loadDetail()
|
||||
} else {
|
||||
} else if (!businessInboundCode.value) {
|
||||
detailRequestSerial += 1
|
||||
subsequentFulfillmentRequestSerial += 1
|
||||
detail.value = null
|
||||
@@ -1095,9 +1197,10 @@ watch(measurementPreviewOpen, (open) => {
|
||||
onMounted(() => {
|
||||
void loadCustomers()
|
||||
void loadWarehouses()
|
||||
if (!orderId.value) void loadOrders()
|
||||
if (!isDetailRoute.value) void loadOrders()
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
detailAssociationRequestSerial += 1
|
||||
measurementRequestSerial += 1
|
||||
releaseMeasurementPreview()
|
||||
releaseCargoImagePreviews()
|
||||
@@ -1107,11 +1210,11 @@ onBeforeUnmount(() => {
|
||||
<template>
|
||||
<AdminPage>
|
||||
<section class="business-page unified-orders-page">
|
||||
<template v-if="orderId">
|
||||
<template v-if="isDetailRoute">
|
||||
<DetailPageLayout ariaLabel="统一订单详情">
|
||||
<header class="order-detail-header">
|
||||
<div class="order-detail-heading">
|
||||
<el-button link class="back-button" @click="closeDetail"><ArrowLeft :size="16" />返回列表</el-button>
|
||||
<el-button link class="back-button" @click="fromMeasurementReview ? returnToMeasurementReviews() : closeDetail()"><ArrowLeft :size="16" />{{ fromMeasurementReview ? '返回测量审核列表' : '返回列表' }}</el-button>
|
||||
<span class="order-mark" aria-hidden="true"><Box :size="19" /></span>
|
||||
<div class="order-title">
|
||||
<h1>{{ detail?.summary.inboundCode || '入仓码暂缺' }}</h1>
|
||||
@@ -1123,8 +1226,9 @@ onBeforeUnmount(() => {
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<LoadErrorState v-if="detailError" class="detail-load-error" title="订单详情暂时无法加载" :message="detailError.message" :loading="detailLoading" @retry="loadDetail">
|
||||
<LoadErrorState v-if="detailError" class="detail-load-error" title="订单详情暂时无法加载" :message="detailError.message" :loading="detailLoading" @retry="businessInboundCode ? resolveBusinessDetailTarget() : loadDetail(true)">
|
||||
<template #details><ApiErrorMeta :details="detailError" /></template>
|
||||
<template v-if="fromMeasurementReview" #actions><el-button type="primary" plain :loading="detailLoading" @click="businessInboundCode ? resolveBusinessDetailTarget() : loadDetail(true)">重新加载</el-button><el-button @click="returnToMeasurementReviews">返回测量审核列表</el-button></template>
|
||||
</LoadErrorState>
|
||||
|
||||
<div v-else v-loading="detailLoading" class="order-detail-content">
|
||||
@@ -1304,6 +1408,7 @@ onBeforeUnmount(() => {
|
||||
<el-alert v-if="!can('platform:firstleg:inbound:measure:view')" title="当前账号缺少测量查看权限" type="warning" :closable="false" show-icon />
|
||||
<LoadErrorState v-else-if="measurementError" title="测量数据暂时无法加载" :message="measurementError.message" :loading="measurementLoading" @retry="loadMeasurementDetails">
|
||||
<template #details><ApiErrorMeta :details="measurementError" /></template>
|
||||
<template v-if="fromMeasurementReview" #actions><el-button type="primary" plain :loading="measurementLoading" @click="loadMeasurementDetails">重新加载</el-button><el-button @click="returnToMeasurementReviews">返回测量审核列表</el-button></template>
|
||||
</LoadErrorState>
|
||||
<template v-else>
|
||||
<section class="measurement-section">
|
||||
@@ -1428,6 +1533,7 @@ onBeforeUnmount(() => {
|
||||
<div v-if="coLoadError" class="coload-error" role="alert"><span>{{ coLoadError.message }}</span><ApiErrorMeta :details="coLoadError" /></div>
|
||||
<DataTable v-model:page="page" v-model:page-size="pageSize" :data="records" :columns="columns" preference-key="unified-orders-table-v2" :loading="loading" :total="total" :page-sizes="[20, 50, 100]" row-key="orderId" border stripe empty-text="暂无符合条件的订单" aria-label="统一订单列表" @selection-change="handleOrderSelectionChange">
|
||||
<template #toolbar-actions>
|
||||
<el-button v-if="can('platform:order:export') && can('platform:download:list')" :loading="exportSubmitting" :disabled="loading || exportSubmitting" @click="exportOrders"><Download :size="16" />导出<span v-if="selectedOrders.length">(已选 {{ selectedOrders.length }})</span></el-button>
|
||||
<el-button v-if="can('platform:order:approve')" type="success" :loading="batchApprovalSaving" :disabled="loading || !selectedApprovalOrders.length" @click="batchApproveOrders"><CircleCheck :size="16" />批量审核通过<span v-if="selectedApprovalOrders.length">({{ selectedApprovalOrders.length }})</span></el-button>
|
||||
<div v-if="can('platform:firstleg:smart-loading:assign')" class="coload-toolbar-action">
|
||||
<el-button type="primary" plain :loading="coLoadSaving" :disabled="loading || !canCreateCoLoadGroup" aria-label="合柜报关" @click="openCoLoadCreate"><Link2 :size="16" />合柜报关<span v-if="selectedOrders.length">({{ selectedOrders.length }})</span></el-button>
|
||||
|
||||
Reference in New Issue
Block a user