Files
admin-web/src/modules/inbounds/components/InboundTaskList.vue
T

500 lines
24 KiB
Vue
Raw Normal View History

2026-08-13 11:57:44 +08:00
<script setup lang="ts">
2026-09-14 15:33:35 +08:00
import '@/styles/adminDensity.css'
import '../measurement/measurementDensity.css'
2026-08-13 19:38:20 +08:00
import { computed, onActivated, onBeforeUnmount, onMounted, ref, watch } from 'vue'
2026-08-13 11:57:44 +08:00
import { useRouter } from 'vue-router'
2026-08-17 18:00:42 +08:00
import { ElMessage, ElMessageBox } from 'element-plus'
2026-09-14 17:54:21 +08:00
import { CircleCheckBig } from '@lucide/vue'
2026-08-13 11:57:44 +08:00
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam'
import { listPlatformWarehouses } from '@/modules/warehouses'
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'
2026-08-13 11:57:44 +08:00
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
import ListPageLayout from '@/shared/components/ListPageLayout.vue'
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
2026-09-03 21:25:56 +08:00
import LineClampText from '@/shared/components/LineClampText.vue'
2026-08-13 11:57:44 +08:00
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
2026-09-02 14:39:34 +08:00
import { firstLegBusinessTypeLabel } from '@/shared/utils/firstLegBusinessLabels'
2026-08-13 11:57:44 +08:00
import { listPlatformInbounds } from '../core/api'
import type { InboundOrderSummary } from '../core/types'
2026-08-17 17:49:10 +08:00
import InboundMeasurementDialog from '../measurement/InboundMeasurementDialog.vue'
2026-08-13 11:57:44 +08:00
import MeasurementReviewPanel from '../review/MeasurementReviewPanel.vue'
2026-08-17 18:00:42 +08:00
import { approveMeasurement, getMeasurementReviewDetail } from '../review/api'
2026-08-13 11:57:44 +08:00
import { inboundProcessStatusLabels, inboundReviewStatusLabels, inboundStatusLabel } from '../status'
2026-08-14 14:27:03 +08:00
import { buildInboundTaskQuery, loadAllActiveCustomers, type InboundTaskMode } from '../task-list'
2026-08-13 11:57:44 +08:00
type FilterModel = QueryModel & {
customerId: string
inboundCode: string
warehouseId: string
expectedArrivalRange: string[]
}
2026-10-19 15:59:14 +08:00
const props = defineProps<{ mode: InboundTaskMode; title?: string }>()
2026-08-13 11:57:44 +08:00
const router = useRouter()
const access = useAccessPreviewStore()
const reviewPanelRef = ref<InstanceType<typeof MeasurementReviewPanel> | null>(null)
2026-08-13 11:57:44 +08:00
const page = ref(1)
const pageSize = ref(20)
const loading = ref(false)
const loadError = ref<ApiErrorDetails | null>(null)
const result = ref({ records: [] as InboundOrderSummary[], total: 0 })
2026-08-14 19:59:32 +08:00
const filters = ref<FilterModel>({ customerId: '', inboundCode: '', warehouseId: '', expectedArrivalRange: [] })
2026-08-13 11:57:44 +08:00
const appliedFilters = ref<FilterModel>({ ...filters.value, expectedArrivalRange: [] })
const customers = ref<PlatformTenantResource[]>([])
const customerLoadError = ref<ApiErrorDetails | null>(null)
const warehouseOptions = ref<Array<{ label: string; value: string }>>([])
const selectedInbound = ref<InboundOrderSummary | null>(null)
2026-08-17 17:49:10 +08:00
const measurementDialogOpen = ref(false)
const reviewDialogOpen = ref(false)
const reviewNavigationLoading = ref(false)
const reviewApproved = ref(false)
const reviewNavigationError = ref<ApiErrorDetails | null>(null)
2026-08-17 18:00:42 +08:00
const batchReviewLoading = ref(false)
const selectedReviewRows = ref<InboundOrderSummary[]>([])
2026-08-13 11:57:44 +08:00
let requestSerial = 0
2026-08-14 14:27:03 +08:00
let optionsRequestSerial = 0
2026-08-13 11:57:44 +08:00
let disposed = false
2026-08-13 19:38:20 +08:00
let activatedOnce = false
2026-08-13 11:57:44 +08:00
const isReview = computed(() => props.mode === 'review')
const actionPermission = computed(() => isReview.value
? 'firstleg:inbound:measurement-review'
: 'platform:firstleg:inbound:measure:view')
const taskLabel = computed(() => isReview.value ? '审核' : '测量')
const emptyText = computed(() => isReview.value ? '暂无待审核的测量数据' : '暂无当前可测量的订单')
const preferenceKey = computed(() => isReview.value ? 'inbound-measurement-reviews' : 'inbound-measurements')
2026-09-18 11:41:27 +08:00
const customerNameById = computed(() => new Map(customers.value.map((customer) => [
String(customer.id), customer.legalName?.trim() || customer.tenantName,
])))
2026-08-13 11:57:44 +08:00
const queryFields = computed<QueryField[]>(() => [
{
key: 'customerId',
type: 'select',
label: '客户',
width: 260,
2026-08-26 18:13:12 +08:00
placeholder: customerLoadError.value ? '客户列表加载失败' : '请选择客户',
2026-08-13 11:57:44 +08:00
options: customers.value.map((customer) => ({
value: String(customer.id),
2026-08-14 12:50:42 +08:00
label: customer.legalName?.trim() || customer.tenantName,
2026-08-13 11:57:44 +08:00
})),
props: { filterable: true, disabled: Boolean(customerLoadError.value) },
},
2026-08-26 18:13:12 +08:00
{ key: 'warehouseId', type: 'select', label: '定位仓库', width: 220, placeholder: '请选择仓库', options: warehouseOptions.value, props: { filterable: true } },
2026-08-13 11:57:44 +08:00
{ key: 'inboundCode', type: 'input', label: '入仓码', width: 220, placeholder: '请输入入仓码' },
{ key: 'expectedArrivalRange', type: 'daterange', label: '预计到达时间', width: 320, placeholder: '请选择日期范围' },
])
2026-08-17 18:00:42 +08:00
const columns = computed<DataTableColumn[]>(() => [
2026-09-03 21:25:56 +08:00
{ key: 'actions', label: '操作', width: 160, fixed: 'left', slotName: 'actions' },
2026-08-17 18:00:42 +08:00
...(isReview.value && access.can(actionPermission.value) ? [{
key: 'selection', type: 'selection', width: 48, fixed: 'left',
selectable: (row: InboundOrderSummary) => row.measureReviewStatus === 'PENDING' && !batchReviewLoading.value,
} satisfies DataTableColumn] : []),
2026-09-03 21:25:56 +08:00
{ key: 'inboundCode', label: '入仓码', minWidth: 140, slotName: 'inboundCode' },
2026-08-13 11:57:44 +08:00
{ key: 'processStatus', label: '流程状态', minWidth: 132, slotName: 'processStatus' },
2026-09-03 21:25:56 +08:00
{ key: 'measureReviewStatus', label: '测量审核状态', minWidth: 120, slotName: 'measureReviewStatus' },
{ key: 'expectedArrivalAt', label: '预计到达时间', minWidth: 140, slotName: 'expectedArrivalAt' },
{ key: 'customerName', label: '客户', minWidth: 140, slotName: 'customerName' },
{ key: 'warehouseName', label: '仓库', minWidth: 120, slotName: 'warehouseName' },
{ key: 'deliveryWarehouseName', label: '交货仓', minWidth: 120, slotName: 'deliveryWarehouseName' },
{ key: 'businessType', label: '业务类型', minWidth: 100, slotName: 'businessType' },
{ key: 'channelName', label: '渠道', minWidth: 100, slotName: 'channelName' },
{ key: 'totalPackageCount', label: '总箱数', width: 90, slotName: 'totalPackageCount' },
{ key: 'palletCount', label: '托盘数', width: 90, slotName: 'palletCount' },
{ key: 'actualGrossWeightKg', label: '实测毛重(kg)', minWidth: 100, slotName: 'actualGrossWeightKg' },
{ key: 'actualNetWeightKg', label: '货物重量(kg)', minWidth: 100, slotName: 'actualNetWeightKg' },
{ key: 'actualVolumeCbm', label: '实测体积(m³)', minWidth: 100, slotName: 'actualVolumeCbm' },
{ key: 'measureCompletedAt', label: '提交时间', minWidth: 140, slotName: 'measureCompletedAt' },
2026-08-17 18:00:42 +08:00
])
2026-08-13 11:57:44 +08:00
function text(value: unknown) {
return value === null || value === undefined || value === '' ? '—' : String(value)
}
2026-09-18 11:41:27 +08:00
function customerDisplayName(inbound: InboundOrderSummary) {
const snapshot = inbound.customerName?.trim() || inbound.companyName?.trim()
return snapshot || customerNameById.value.get(String(inbound.customerId || inbound.tenantId)) || ''
}
2026-08-13 11:57:44 +08:00
function formatNumber(value: number | null | undefined) {
return value === null || value === undefined ? '—' : value.toLocaleString('zh-CN', { maximumFractionDigits: 3 })
}
function formatDate(value: string | null | undefined) {
if (!value) return '—'
const date = new Date(value)
return Number.isNaN(date.getTime()) ? value : date.toLocaleString('zh-CN', { hour12: false })
}
2026-09-04 15:06:39 +08:00
function formatDateOnly(value: string | null | undefined) {
if (!value) return '—'
const date = new Date(value)
return Number.isNaN(date.getTime()) ? value : date.toLocaleDateString('zh-CN')
}
2026-08-13 11:57:44 +08:00
function dateParam(value: string | undefined, end = false) {
return value ? `${value}T${end ? '23:59:59' : '00:00:00'}` : undefined
}
async function loadOptions() {
2026-08-14 14:27:03 +08:00
const serial = ++optionsRequestSerial
2026-08-13 11:57:44 +08:00
customerLoadError.value = null
try {
2026-08-14 14:27:03 +08:00
const [customerRecords, warehousePage] = await Promise.all([
loadAllActiveCustomers((query) => listPlatformTenants(query)),
2026-08-13 11:57:44 +08:00
listPlatformWarehouses(1, 200),
])
2026-08-14 14:27:03 +08:00
if (disposed || serial !== optionsRequestSerial) return
customers.value = customerRecords
2026-08-13 11:57:44 +08:00
warehouseOptions.value = warehousePage.records.map((warehouse) => ({
label: `${warehouse.warehouseName} · ${warehouse.warehouseCode}`,
value: String(warehouse.id),
}))
} catch (error) {
if (disposed) return
customerLoadError.value = getApiErrorDetails(error, '客户或仓库筛选项加载失败,不影响查看全部待办')
}
}
async function loadTasks() {
2026-08-17 18:00:42 +08:00
selectedReviewRows.value = []
2026-08-13 11:57:44 +08:00
const serial = ++requestSerial
loading.value = true
loadError.value = null
const applied = appliedFilters.value
try {
const response = await listPlatformInbounds(applied.customerId || undefined, buildInboundTaskQuery(props.mode, {
page: page.value,
size: pageSize.value,
warehouseId: applied.warehouseId || undefined,
inboundCode: applied.inboundCode.trim() || undefined,
expectedArrivalFrom: dateParam(applied.expectedArrivalRange[0]),
expectedArrivalTo: dateParam(applied.expectedArrivalRange[1], true),
}))
if (disposed || serial !== requestSerial) return
result.value = { records: response.records, total: response.total }
} catch (error) {
if (disposed || serial !== requestSerial) return
loadError.value = getApiErrorDetails(error, `${taskLabel.value}待办加载失败`)
} finally {
if (!disposed && serial === requestSerial) loading.value = false
}
}
function applyFilters() {
2026-08-26 18:13:12 +08:00
appliedFilters.value = {
...filters.value,
expectedArrivalRange: Array.isArray(filters.value.expectedArrivalRange) ? [...filters.value.expectedArrivalRange] : [],
}
2026-08-13 11:57:44 +08:00
if (page.value === 1) void loadTasks()
else page.value = 1
}
function resetFilters() {
2026-08-14 19:59:32 +08:00
filters.value = { customerId: '', inboundCode: '', warehouseId: '', expectedArrivalRange: [] }
2026-08-13 11:57:44 +08:00
appliedFilters.value = { ...filters.value, expectedArrivalRange: [] }
if (page.value === 1) void loadTasks()
else page.value = 1
}
function openTask(row: InboundOrderSummary) {
selectedInbound.value = row
if (isReview.value) {
reviewApproved.value = false
reviewNavigationError.value = null
reviewDialogOpen.value = true
}
2026-08-17 17:49:10 +08:00
else measurementDialogOpen.value = true
2026-08-13 11:57:44 +08:00
}
function openDetail(row: InboundOrderSummary) {
2026-08-13 19:00:37 +08:00
if (!row.orderId) return
void router.push({ name: 'unified-order-detail', params: { orderId: row.orderId }, query: { tenantId: row.tenantId } })
2026-08-13 11:57:44 +08:00
}
async function handleTaskChanged() {
const target = selectedInbound.value
if (!isReview.value) {
reviewDialogOpen.value = false
selectedInbound.value = null
void loadTasks()
return
}
if (!target || reviewNavigationLoading.value) return
reviewApproved.value = true
reviewNavigationError.value = null
const inboundCode = target.inboundCode?.trim()
const orderId = target.orderId?.trim()
if (!inboundCode && !orderId) {
reviewNavigationError.value = { message: '审核已通过,但未找到对应订单,请关闭弹框后刷新列表重试' }
return
}
reviewNavigationLoading.value = true
try {
if (inboundCode) {
await router.push({
name: 'unified-order-detail-by-inbound-code',
params: { inboundCode },
query: { tab: 'measurement', from: 'measurement-review' },
})
} else {
await router.push({
name: 'unified-order-detail',
params: { orderId },
query: { tenantId: target.tenantId, tab: 'measurement', from: 'measurement-review' },
})
}
reviewDialogOpen.value = false
selectedInbound.value = null
} catch (error) {
reviewNavigationError.value = getApiErrorDetails(error, '订单详情打开失败,请重试')
} finally {
reviewNavigationLoading.value = false
}
2026-08-13 11:57:44 +08:00
}
2026-08-17 18:00:42 +08:00
function handleReviewSelectionChange(rows: InboundOrderSummary[]) {
selectedReviewRows.value = rows.filter((row) => row.measureReviewStatus === 'PENDING')
}
async function batchApproveMeasurements() {
const selected = [...selectedReviewRows.value]
if (!selected.length || batchReviewLoading.value) return
batchReviewLoading.value = true
try {
await ElMessageBox.confirm(
`确认批量审核通过已勾选的 ${selected.length} 条测量数据吗?一件代发通过后进入待组托,其他订单进入待分柜。`,
2026-08-17 18:00:42 +08:00
'批量审核测量数据',
2026-09-14 15:52:00 +08:00
{ customClass: 'admin-density-confirm', confirmButtonText: '确认批量审核', cancelButtonText: '取消', type: 'warning' },
2026-08-17 18:00:42 +08:00
)
let successCount = 0
let failureCount = 0
const failures: string[] = []
2026-08-17 18:04:08 +08:00
const pricingWarnings: string[] = []
2026-08-17 18:00:42 +08:00
for (const row of selected) {
try {
const detail = await getMeasurementReviewDetail(String(row.tenantId), String(row.id))
2026-08-17 18:04:08 +08:00
const approval = await approveMeasurement(String(row.tenantId), String(row.id), {
2026-08-17 18:00:42 +08:00
measurementId: detail.measurementId,
2026-08-27 21:48:15 +08:00
expectedVersion: detail.inboundVersion,
2026-08-17 18:00:42 +08:00
})
successCount += 1
2026-08-17 18:04:08 +08:00
if (!approval.initialPricingSucceeded) {
pricingWarnings.push(
2026-08-30 17:43:03 +08:00
`${row.inboundCode || '入仓码待补充'}:${approval.initialPricingMessage || '费用初算未生成'}`,
2026-08-17 18:04:08 +08:00
)
}
2026-08-17 18:00:42 +08:00
} catch (error) {
failureCount += 1
2026-08-30 17:43:03 +08:00
failures.push(getApiErrorDetails(error, `${row.inboundCode || '入仓码待补充'} 审核失败`).message)
2026-08-17 18:00:42 +08:00
}
}
await loadTasks()
const reason = [...new Set(failures.filter(Boolean))].slice(0, 3).join(';')
2026-08-17 18:04:08 +08:00
const pricingReason = [...new Set(pricingWarnings.filter(Boolean))].slice(0, 3).join(';')
if (!failureCount && !pricingWarnings.length) ElMessage.success(`已批量审核通过 ${successCount} 条测量数据,费用初算已生成`)
else if (!failureCount) ElMessage.warning(`已批量审核通过 ${successCount} 条测量数据;其中 ${pricingWarnings.length} 条费用初算未生成,可补价后重新计算${pricingReason ? `。${pricingReason}` : ''}`)
2026-08-17 18:00:42 +08:00
else if (successCount) ElMessage.warning(`批量审核完成:成功 ${successCount} 条,失败 ${failureCount} 条${reason ? `。${reason}` : ''}`)
else ElMessage.error(`批量审核失败:${failureCount} 条测量数据未通过${reason ? `。${reason}` : ''}`)
} catch (error) {
if (error !== 'cancel' && error !== 'close') ElMessage.error(getApiErrorDetails(error, '批量审核失败').message)
} finally {
batchReviewLoading.value = false
}
}
2026-08-17 11:23:31 +08:00
function handleMeasurementUpdated() {
void loadTasks()
}
2026-08-17 17:49:10 +08:00
function handleMeasurementSubmitted() {
void loadTasks()
}
function clearSelectedInbound() {
selectedInbound.value = null
reviewApproved.value = false
reviewNavigationError.value = null
reviewNavigationLoading.value = false
2026-08-14 14:50:44 +08:00
}
2026-08-13 11:57:44 +08:00
watch([page, pageSize], () => { void loadTasks() })
onMounted(() => { void loadOptions(); void loadTasks() })
2026-08-13 19:38:20 +08:00
onActivated(() => {
if (!activatedOnce) {
activatedOnce = true
return
}
void loadTasks()
})
2026-08-14 14:27:03 +08:00
onBeforeUnmount(() => { disposed = true; requestSerial += 1; optionsRequestSerial += 1 })
2026-08-13 11:57:44 +08:00
</script>
<template>
2026-09-14 15:33:35 +08:00
<AdminPage as="div" class="inbound-task-page admin-density">
2026-08-13 11:57:44 +08:00
<ListPageLayout :aria-label="`${taskLabel}待办`">
2026-09-30 14:01:58 +08:00
<section :aria-busy="loading"><DataTable appearance="integrated" :stripe="false" column-settings-mode="show" :results-visible="!loadError"
2026-08-13 11:57:44 +08:00
v-model:page="page"
v-model:page-size="pageSize"
:data="result.records"
:columns="columns"
:preference-key="`${preferenceKey}-table`"
:loading="loading"
:total="result.total"
:page-sizes="[20, 50, 100]"
row-key="id"
:empty-text="emptyText"
:aria-label="`${taskLabel}待办列表`"
2026-08-17 18:00:42 +08:00
@selection-change="handleReviewSelectionChange"
2026-08-13 11:57:44 +08:00
>
2026-10-19 15:59:14 +08:00
<template #toolbar-start><h1>{{ title || (isReview ? '测量数据审核' : '订单测量') }}</h1></template>
2026-09-30 14:01:58 +08:00
<template #filters>
2026-10-19 15:59:14 +08:00
<slot name="status-tabs" />
2026-09-30 14:01:58 +08:00
<QueryToolbar
v-model="filters"
:ariaLabel="`${taskLabel}待办筛选`"
:fields="queryFields"
:preference-key="`${preferenceKey}-query`"
:search-on-reset="false"
@search="applyFilters"
@reset="resetFilters"
/>
</template>
<template #notices><template v-if="customerLoadError || loadError">
<div class="state-stack">
<el-alert v-if="customerLoadError" type="warning" :closable="false" show-icon>
<template #title><span>{{ customerLoadError.message }}</span></template>
</el-alert>
<LoadErrorState v-if="loadError" :title="`${taskLabel}待办暂时无法加载`" :message="loadError.message" @retry="loadTasks">
<template #details><ApiErrorMeta :details="loadError" /></template>
</LoadErrorState>
</div>
</template></template>
2026-08-17 18:00:42 +08:00
<template v-if="isReview && access.can(actionPermission)" #toolbar-actions>
<el-button type="success" :loading="batchReviewLoading" :disabled="loading || !selectedReviewRows.length" @click="batchApproveMeasurements"><CircleCheckBig :size="16" />批量审核通过<span v-if="selectedReviewRows.length">({{ selectedReviewRows.length }})</span></el-button>
</template>
2026-08-13 11:57:44 +08:00
<template #pagination-left>当前显示 {{ result.records.length }} 条 · 共 {{ result.total }} 条</template>
2026-09-30 14:01:58 +08:00
<template #actions="{ row }"><div class="row-actions density-actions admin-text-actions">
2026-09-14 17:54:21 +08:00
<el-button v-if="access.can(actionPermission)" class="density-action" :data-tone="isReview ? 'warning' : 'purple'" size="small" @click="openTask(row as InboundOrderSummary)">
2026-08-13 11:57:44 +08:00
{{ taskLabel }}
</el-button>
2026-09-14 17:54:21 +08:00
<el-button v-if="access.can('platform:firstleg:inbound:view') && (row as InboundOrderSummary).orderId" class="density-action" size="small" @click="openDetail(row as InboundOrderSummary)">
查看
2026-08-13 11:57:44 +08:00
</el-button>
2026-08-13 19:00:37 +08:00
<span v-if="!access.can(actionPermission) && (!access.can('platform:firstleg:inbound:view') || !(row as InboundOrderSummary).orderId)" class="muted-action">无操作权限</span>
2026-09-30 14:01:58 +08:00
</div></template>
2026-09-02 14:39:34 +08:00
<template #inboundCode="{ row }"><div class="inbound-code-cell"><strong>{{ text((row as InboundOrderSummary).inboundCode) }}</strong></div></template>
2026-08-13 11:57:44 +08:00
<template #processStatus="{ row }"><el-tag effect="light">{{ inboundStatusLabel((row as InboundOrderSummary).processStatus, inboundProcessStatusLabels) }}</el-tag></template>
<template #measureReviewStatus="{ row }">{{ inboundStatusLabel((row as InboundOrderSummary).measureReviewStatus, inboundReviewStatusLabels) }}</template>
2026-09-04 15:06:39 +08:00
<template #expectedArrivalAt="{ row }">{{ formatDateOnly((row as InboundOrderSummary).expectedArrivalAt) }}</template>
2026-09-03 21:25:56 +08:00
<template #customerName="{ row }"><LineClampText :content="(row as InboundOrderSummary).customerName" /></template>
2026-08-13 11:57:44 +08:00
<template #warehouseName="{ row }">{{ text((row as InboundOrderSummary).warehouseName) }}</template>
<template #deliveryWarehouseName="{ row }">{{ text((row as InboundOrderSummary).deliveryWarehouseName) }}</template>
2026-09-02 14:39:34 +08:00
<template #businessType="{ row }">{{ firstLegBusinessTypeLabel((row as InboundOrderSummary).businessType) }}</template>
2026-09-03 21:25:56 +08:00
<template #channelName="{ row }"><LineClampText :content="(row as InboundOrderSummary).channelName || (row as InboundOrderSummary).channelCode" /></template>
2026-08-13 11:57:44 +08:00
<template #totalPackageCount="{ row }">{{ text((row as InboundOrderSummary).totalPackageCount) }}</template>
2026-08-17 19:13:25 +08:00
<template #palletCount="{ row }">{{ text((row as InboundOrderSummary).palletCount) }}</template>
2026-08-13 11:57:44 +08:00
<template #actualGrossWeightKg="{ row }">{{ formatNumber((row as InboundOrderSummary).actualGrossWeightKg) }}</template>
2026-08-17 19:13:25 +08:00
<template #actualNetWeightKg="{ row }">{{ formatNumber((row as InboundOrderSummary).actualNetWeightKg) }}</template>
2026-08-13 11:57:44 +08:00
<template #actualVolumeCbm="{ row }">{{ formatNumber((row as InboundOrderSummary).actualVolumeCbm) }}</template>
<template #measureCompletedAt="{ row }">{{ formatDate((row as InboundOrderSummary).measureCompletedAt) }}</template>
<template #empty><el-empty :description="emptyText" /></template>
2026-09-30 14:01:58 +08:00
</DataTable></section>
2026-08-13 11:57:44 +08:00
</ListPageLayout>
<AppDialog
2026-08-17 17:49:10 +08:00
v-if="isReview"
v-model="reviewDialogOpen"
2026-08-30 17:43:03 +08:00
:title="selectedInbound ? `${taskLabel} · ${selectedInbound.inboundCode || '入仓码待补充'}` : taskLabel"
2026-09-14 15:33:35 +08:00
width="min(1560px, calc(100vw - 48px))"
2026-08-13 11:57:44 +08:00
top="4vh"
fullscreenable
:show-confirm="false"
2026-08-13 11:57:44 +08:00
destroy-on-close
append-to-body
:close-on-click-modal="!reviewNavigationLoading"
:close-on-press-escape="!reviewNavigationLoading"
2026-09-14 15:33:35 +08:00
class="inbound-task-dialog admin-density-dialog admin-density-workspace inbound-density"
2026-08-17 17:49:10 +08:00
@closed="clearSelectedInbound"
2026-08-13 11:57:44 +08:00
>
<el-alert v-if="reviewNavigationError" type="error" :closable="false" show-icon class="review-navigation-error">
<template #title>{{ reviewNavigationError.message }}</template>
<ApiErrorMeta :details="reviewNavigationError" />
<el-button v-if="selectedInbound?.inboundCode || selectedInbound?.orderId" link type="danger" :loading="reviewNavigationLoading" @click="handleTaskChanged">重新打开订单详情</el-button>
</el-alert>
2026-08-13 11:57:44 +08:00
<MeasurementReviewPanel
2026-08-17 17:49:10 +08:00
v-if="selectedInbound"
ref="reviewPanelRef"
:show-actions="false"
2026-08-13 17:14:51 +08:00
:tenant-id="selectedInbound.tenantId"
2026-08-13 11:57:44 +08:00
:inbound-id="selectedInbound.id"
:fulfillment-type="selectedInbound.fulfillmentType"
2026-08-18 11:41:14 +08:00
:customer-name="selectedInbound.customerName || selectedInbound.companyName"
:channel-name="selectedInbound.channelName"
:channel-code="selectedInbound.channelCode"
:post-approve-loading="reviewNavigationLoading"
:approval-completed="reviewApproved"
2026-08-13 11:57:44 +08:00
@changed="handleTaskChanged"
/>
<template #footer="{ close }">
<div class="review-dialog-footer">
<el-button :disabled="reviewNavigationLoading" @click="close">关闭</el-button>
<template v-if="reviewPanelRef?.isPending && reviewPanelRef?.canReview">
<el-button type="primary" plain @click="reviewPanelRef?.enableWeightCorrection()">修改重量</el-button>
<el-button type="primary" :loading="reviewPanelRef?.actionLoading === 'approve' || reviewNavigationLoading" :disabled="reviewApproved" @click="reviewPanelRef?.confirmApprove()">{{ reviewApproved ? '审核已通过' : reviewPanelRef?.approvalButtonLabel }}</el-button>
</template>
</div>
</template>
</AppDialog>
2026-08-17 17:49:10 +08:00
<InboundMeasurementDialog
v-if="!isReview && selectedInbound"
v-model="measurementDialogOpen"
:customer-id="selectedInbound.tenantId"
2026-09-18 11:41:27 +08:00
:customer-name="customerDisplayName(selectedInbound)"
2026-08-17 17:49:10 +08:00
:inbound-id="selectedInbound.id"
:inbound-code="selectedInbound.inboundCode"
@updated="handleMeasurementUpdated"
@submitted="handleMeasurementSubmitted"
@closed="clearSelectedInbound"
/>
2026-08-13 11:57:44 +08:00
</AdminPage>
</template>
<style scoped>
.review-dialog-footer { display: flex; align-items: center; justify-content: flex-end; flex-wrap: wrap; gap: var(--admin-space-4, 10px); }
.review-dialog-footer :deep(.el-button) { margin-left: 0; }
2026-08-13 11:57:44 +08:00
.inbound-task-page { min-width: 0; }
2026-09-14 17:54:21 +08:00
.state-stack { display: grid; gap: var(--admin-space-5, 12px); }
.review-navigation-error { margin-bottom: 14px; }
2026-09-14 17:54:21 +08:00
.row-actions { white-space: nowrap; }
2026-08-13 11:57:44 +08:00
.primary-cell { display: grid; gap: 3px; min-width: 0; }
.primary-cell strong, .primary-cell span { overflow-wrap: anywhere; }
2026-08-30 17:43:03 +08:00
.inbound-code-cell { display: grid; gap: 3px; min-width: 0; }
2026-09-14 17:54:21 +08:00
.primary-cell span, .muted-action { color: var(--cc-muted); font-size: var(--admin-font-caption, 12px); }
2026-08-13 11:57:44 +08:00
@media (max-width: 520px) {
.row-actions { align-items: stretch; flex-direction: column; }
.row-actions :deep(.el-button) { width: 100%; }
}
</style>
<style>
2026-09-14 15:33:35 +08:00
.inbound-task-dialog.admin-density-workspace.el-dialog:not(.is-fullscreen) { height: auto; max-height: 92vh; overflow: hidden; }
2026-08-13 11:57:44 +08:00
@media (max-width: 520px) {
.inbound-task-dialog .el-dialog__body { padding: 14px; }
}
</style>