Files
admin-web/src/modules/first-leg-orders/components/PlatformFirstLegOrdersPanel.vue
T
2026-08-10 16:24:54 +08:00

613 lines
29 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
import { ArrowLeft, CircleX, Eye, FilePenLine, MoreHorizontal, PackageCheck } from '@lucide/vue'
import { useRoute, useRouter } from 'vue-router'
import {
acceptPlatformFirstLegOrder,
addPlatformFirstLegOrderNote,
approvePlatformFirstLegOrder,
cancelPlatformFirstLegOrder,
completePlatformFirstLegOrder,
getPlatformFirstLegOrder,
listPlatformFirstLegOrders,
startPlatformFirstLegOrder,
updatePlatformFirstLegOrderExpress,
type PlatformFirstLegOrderDetailResource,
type PlatformFirstLegOrderResource,
type PlatformFirstLegOrderStatus,
} from '@/modules/first-leg-orders/api/platformFirstLegOrders'
import { ApiResponseError, getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
import AdminPage from '@/shared/components/AdminPage.vue'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
import DetailPageLayout from '@/shared/components/DetailPageLayout.vue'
import StatusBadge from '@/shared/components/StatusBadge.vue'
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
import ListPageLayout from '@/shared/components/ListPageLayout.vue'
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam'
const route = useRoute()
const router = useRouter()
const access = useAccessPreviewStore()
const orders = ref<PlatformFirstLegOrderResource[]>([])
const tenants = ref<PlatformTenantResource[]>([])
const tenantLoadError = ref<ApiErrorDetails | null>(null)
const total = ref(0)
const page = ref(1)
const pageSize = ref(20)
const loading = ref(false)
const actionLoadingId = ref('')
const loadError = ref<ApiErrorDetails | null>(null)
const detailLoading = ref(false)
const detail = ref<PlatformFirstLegOrderDetailResource | null>(null)
const noteDialogOpen = ref(false)
const expressDialogOpen = ref(false)
const selectedOrder = ref<PlatformFirstLegOrderResource | null>(null)
const noteForm = reactive({ content: '' })
const noteSaving = ref(false)
const expressSaving = ref(false)
const expressFormRef = ref<FormInstance>()
const noteFormRef = ref<FormInstance>()
const expressForm = reactive({ expressCompany: '', expressTrackingNo: '' })
const filters = ref<QueryModel & {
tenantId: string
keyword: string
status: PlatformFirstLegOrderStatus | ''
dateRange: string[]
}>({
tenantId: '',
keyword: '',
status: 'SUBMITTED',
dateRange: [],
})
const queryFields = computed<QueryField[]>(() => [
{
key: 'tenantId',
type: 'select',
label: '企业客户',
width: 160,
placeholder: tenantLoadError.value ? '企业列表加载失败' : '全部企业',
props: { 'aria-label': '企业客户', filterable: true, disabled: Boolean(tenantLoadError.value) },
options: tenants.value.map((tenant) => ({ value: String(tenant.id), label: `${tenant.tenantName}(${tenant.tenantCode || tenant.id})` })),
},
{
key: 'status',
type: 'select',
label: '订单状态',
width: 120,
placeholder: '全部状态',
props: { 'aria-label': '订单状态' },
options: [
{ value: 'DRAFT', label: '草稿' },
{ value: 'SUBMITTED', label: '待审核' },
{ value: 'APPROVED', label: '已审核' },
{ value: 'ACCEPTED', label: '已受理' },
{ value: 'PROCESSING', label: '处理中' },
{ value: 'COMPLETED', label: '已完成' },
{ value: 'CANCELLED', label: '已取消' },
{ value: 'REJECTED', label: '已驳回(旧契约)' },
],
},
{ key: 'keyword', type: 'input', label: '订单关键词', placeholder: '订单号或发货客户名称', props: { 'aria-label': '订单关键词' } },
{ key: 'dateRange', type: 'daterange', label: '订单创建日期', props: { 'aria-label': '订单创建日期' } },
])
const orderColumns: DataTableColumn[] = [
{ key: 'actions', label: '操作', minWidth: 224, fixed: 'left', slotName: 'actions' },
{ key: 'status', label: '状态', width: 108, slotName: 'status' },
{ key: 'orderInfo', label: '订单信息', minWidth: 210, slotName: 'orderInfo' },
{ key: 'customer', label: '客户', minWidth: 190, showOverflowTooltip: true, slotName: 'customer' },
{ key: 'transport', label: '运输方案', minWidth: 180, slotName: 'transport' },
{ key: 'destination', label: '目的地', minWidth: 150, slotName: 'destination' },
{ key: 'receivingWarehouse', label: '平台收货仓', minWidth: 180, slotName: 'receivingWarehouse' },
{ key: 'express', label: '履约快递', minWidth: 160, slotName: 'express' },
]
const expressRules: FormRules = {
expressCompany: [{ required: true, message: '请填写快递公司', trigger: 'blur' }],
expressTrackingNo: [
{ required: true, message: '请填写快递单号', trigger: 'blur' },
{ pattern: /^[A-Za-z0-9-]{4,128}$/, message: '快递单号格式不正确', trigger: 'blur' },
],
}
const noteRules: FormRules = {
content: [{ required: true, message: '请填写内部备注', trigger: 'blur' }],
}
const detailOrderId = computed(() => {
const orderId = route.query.orderId
return typeof orderId === 'string' && orderId ? orderId : ''
})
const detailTenantId = computed(() => {
const tenantId = route.query.tenantId
return typeof tenantId === 'string' && tenantId ? tenantId : ''
})
const isDetailPage = computed(() => Boolean(detailOrderId.value))
const statusOptions: Array<{ value: PlatformFirstLegOrderStatus; label: string }> = [
{ value: 'DRAFT', label: '草稿' },
{ value: 'SUBMITTED', label: '待审核' },
{ value: 'APPROVED', label: '已审核' },
{ value: 'ACCEPTED', label: '已受理' },
{ value: 'PROCESSING', label: '处理中' },
{ value: 'COMPLETED', label: '已完成' },
{ value: 'CANCELLED', label: '已取消' },
{ value: 'REJECTED', label: '已驳回(旧契约)' },
]
function statusLabel(status: PlatformFirstLegOrderStatus) {
return statusOptions.find((item) => item.value === status)?.label || status
}
function transportLabel(order: PlatformFirstLegOrderResource) {
if (order.transportMode === 'SEA') return order.containerMode === 'FULL_CONTAINER' ? '海运整箱 FCL' : '海运拼箱 LCL'
if (order.transportMode === 'AIR') return '空运'
if (order.transportMode === 'RAIL') return '铁路运输'
return '陆运'
}
function formattedDate(value: string | null | undefined) {
return value ? value.replace('T', ' ').slice(0, 16) : '-'
}
function tenantFor(tenantId: number | string) {
return tenants.value.find((tenant) => String(tenant.id) === String(tenantId))
}
function tenantName(tenantId: number | string) {
return tenantFor(tenantId)?.tenantName || `企业 ${tenantId}`
}
function customerDisplay(order: PlatformFirstLegOrderResource) {
const tenant = tenantFor(order.tenantId)
const name = tenant?.legalName?.trim() || tenant?.tenantName?.trim() || order.customerName?.trim() || '未命名企业'
const code = tenant?.tenantCode?.trim()
const secondary = code ? `客户编码 ${code}` : order.customerName?.trim()
return { name, secondary: secondary && secondary !== name ? secondary : '' }
}
async function loadTenants() {
tenantLoadError.value = null
try {
const records: PlatformTenantResource[] = []
let currentPage = 1
let pages = 1
do {
const result = await listPlatformTenants({ page: currentPage, size: 200 })
records.push(...result.records)
pages = Math.max(1, result.pages)
currentPage += 1
} while (currentPage <= pages)
tenants.value = records
} catch (error) {
tenants.value = []
tenantLoadError.value = getApiErrorDetails(error, '企业列表加载失败,暂时无法按企业筛选;订单仍可正常审核')
}
}
function can(action: string) {
return access.can(action)
}
function canCancel(order: PlatformFirstLegOrderResource) {
return can('platform:order:cancel') && ['DRAFT', 'SUBMITTED', 'APPROVED', 'ACCEPTED', 'REJECTED'].includes(order.status)
}
function canMaintainExpress(order: PlatformFirstLegOrderResource) {
return can('platform:order:fulfillment') && !['DRAFT', 'REJECTED', 'CANCELLED'].includes(order.status)
}
function hasSecondaryActions(order: PlatformFirstLegOrderResource) {
return canCancel(order) || canMaintainExpress(order) || can('platform:order:note')
}
function runSecondaryAction(command: string | number | object, order: PlatformFirstLegOrderResource) {
if (command === 'express') openExpress(order)
if (command === 'note') openNote(order)
if (command === 'cancel') void cancelOrder(order)
}
function actionLabel(order: PlatformFirstLegOrderResource) {
if (order.status === 'SUBMITTED' && can('platform:order:approve')) return '审核通过'
if (order.status === 'APPROVED' && can('platform:order:accept')) return '受理订单'
if (order.status === 'ACCEPTED' && can('platform:order:process')) return '开始处理'
if (order.status === 'PROCESSING' && can('platform:order:complete')) return '完成订单'
return ''
}
async function loadOrders() {
loadError.value = null
loading.value = true
try {
const result = await listPlatformFirstLegOrders({
tenantId: filters.value.tenantId || undefined,
keyword: filters.value.keyword.trim() || undefined,
status: filters.value.status || undefined,
createdFrom: filters.value.dateRange.length === 2 ? `${filters.value.dateRange[0]}T00:00:00` : undefined,
createdTo: filters.value.dateRange.length === 2 ? `${filters.value.dateRange[1]}T23:59:59` : undefined,
page: page.value,
size: pageSize.value,
})
orders.value = result.records
total.value = result.total
} catch (error) {
orders.value = []
total.value = 0
loadError.value = getApiErrorDetails(error, '订单列表加载失败,请检查 Gateway、租户范围和订单权限')
} finally {
loading.value = false
}
}
function searchOrders() {
if (page.value !== 1) {
page.value = 1
return
}
void loadOrders()
}
async function resetOrders() {
filters.value = { ...filters.value, status: 'SUBMITTED' }
await nextTick()
searchOrders()
}
watch([page, pageSize], () => {
void loadOrders()
})
async function openDetail(order: PlatformFirstLegOrderResource) {
selectedOrder.value = order
await router.push({
path: route.path,
query: { ...route.query, orderId: String(order.id), tenantId: String(order.tenantId) },
})
}
async function loadDetail(orderId: string, tenantId = detailTenantId.value) {
if (!tenantId) {
detail.value = null
ElMessage.error('订单详情缺少企业租户标识,请从订单列表重新打开')
return
}
detail.value = null
detailLoading.value = true
try {
detail.value = await getPlatformFirstLegOrder(tenantId, orderId)
selectedOrder.value = detail.value.order
} catch (error) {
const details = getApiErrorDetails(error, '订单详情加载失败')
ElMessage.error(details.message)
} finally {
detailLoading.value = false
}
}
async function refreshAfterAction(orderId: string) {
await loadOrders()
if (detailOrderId.value === orderId) {
await loadDetail(orderId)
}
}
function closeDetailPage() {
const { orderId: _orderId, tenantId: _tenantId, ...query } = route.query
void router.replace({ path: route.path, query })
}
function getFieldValidationMessage(error: unknown, field: string, fallback: string) {
const details = getApiErrorDetails(error, fallback)
if (error instanceof ApiResponseError && error.details && typeof error.details === 'object') {
const fieldMessage = (error.details as Record<string, unknown>)[field]
if (typeof fieldMessage === 'string' && fieldMessage) {
return `${details.message}:${fieldMessage}`
}
}
return details.message
}
async function runLifecycleAction(order: PlatformFirstLegOrderResource) {
const label = actionLabel(order)
if (!label) return
try {
await ElMessageBox.confirm(`确认${label}“${order.orderNo || order.id}”吗?该操作会写入订单状态历史。`, label, {
confirmButtonText: `确认${label}`,
cancelButtonText: '取消',
type: 'warning',
})
actionLoadingId.value = String(order.id)
const tenantId = String(order.tenantId)
if (order.status === 'SUBMITTED') await approvePlatformFirstLegOrder(tenantId, String(order.id))
if (order.status === 'APPROVED') await acceptPlatformFirstLegOrder(tenantId, String(order.id))
if (order.status === 'ACCEPTED') await startPlatformFirstLegOrder(tenantId, String(order.id))
if (order.status === 'PROCESSING') await completePlatformFirstLegOrder(tenantId, String(order.id))
await refreshAfterAction(String(order.id))
ElMessage.success(`${label}已完成`)
} catch (error) {
if (error === 'cancel' || error === 'close') return
ElMessage.error(getApiErrorDetails(error, `${label}失败,请稍后重试`).message)
} finally {
actionLoadingId.value = ''
}
}
async function cancelOrder(order: PlatformFirstLegOrderResource) {
try {
const result = await ElMessageBox.prompt('请填写取消原因。后端要求该字段必填,并将写入订单状态历史。', '取消订单', {
confirmButtonText: '确认取消',
cancelButtonText: '返回',
inputPattern: /\S+/,
inputErrorMessage: '请填写取消原因',
inputPlaceholder: '例如:客户取消本次运输安排',
})
actionLoadingId.value = String(order.id)
await cancelPlatformFirstLegOrder(String(order.tenantId), String(order.id), result.value.trim())
await refreshAfterAction(String(order.id))
ElMessage.success('订单已取消')
} catch (error) {
if (error === 'cancel' || error === 'close') return
ElMessage.error(getApiErrorDetails(error, '取消订单失败,请稍后重试').message)
} finally {
actionLoadingId.value = ''
}
}
function openExpress(order: PlatformFirstLegOrderResource) {
selectedOrder.value = order
expressForm.expressCompany = order.expressCompany || ''
expressForm.expressTrackingNo = order.expressTrackingNo || ''
expressDialogOpen.value = true
void nextTick(() => expressFormRef.value?.clearValidate())
}
async function saveExpress() {
if (!selectedOrder.value) return
const valid = await expressFormRef.value?.validate().catch(() => false)
if (!valid) return
expressSaving.value = true
try {
await updatePlatformFirstLegOrderExpress(String(selectedOrder.value.tenantId), String(selectedOrder.value.id), {
expressCompany: expressForm.expressCompany.trim(),
expressTrackingNo: expressForm.expressTrackingNo.trim(),
})
expressDialogOpen.value = false
await refreshAfterAction(String(selectedOrder.value.id))
ElMessage.success('履约快递信息已保存')
} catch (error) {
ElMessage.error(getFieldValidationMessage(error, 'expressTrackingNo', '履约快递信息保存失败'))
} finally {
expressSaving.value = false
}
}
function openNote(order: PlatformFirstLegOrderResource) {
selectedOrder.value = order
noteForm.content = ''
noteDialogOpen.value = true
void nextTick(() => noteFormRef.value?.clearValidate())
}
async function saveNote() {
if (!selectedOrder.value) return
const valid = await noteFormRef.value?.validate().catch(() => false)
if (!valid) return
noteSaving.value = true
try {
await addPlatformFirstLegOrderNote(String(selectedOrder.value.tenantId), String(selectedOrder.value.id), noteForm.content.trim())
noteDialogOpen.value = false
await refreshAfterAction(String(selectedOrder.value.id))
ElMessage.success('内部备注已保存')
} catch (error) {
ElMessage.error(getApiErrorDetails(error, '内部备注保存失败').message)
} finally {
noteSaving.value = false
}
}
watch([detailOrderId, detailTenantId], ([orderId, tenantId]) => {
if (orderId) {
void loadDetail(orderId, tenantId)
return
}
detail.value = null
selectedOrder.value = null
void loadOrders()
}, { immediate: true })
onMounted(() => void loadTenants())
</script>
<template>
<AdminPage>
<div class="business-page">
<template v-if="isDetailPage">
<section class="surface-panel detail-page" v-loading="detailLoading">
<DetailPageLayout ariaLabel="头程订单详情">
<template #header>
<header class="detail-page-header">
<div>
<h1>订单详情</h1>
<p>查看订单信息、履约快递、内部备注和状态历史。</p>
</div>
<el-button @click="closeDetailPage"><ArrowLeft :size="16" />返回订单列表</el-button>
</header>
</template>
<template v-if="detail">
<div class="order-detail-head">
<div>
<span>{{ transportLabel(detail.order) }}</span>
<h2>{{ detail.order.orderNo || detail.order.id }}</h2>
<p>{{ tenantName(detail.order.tenantId) }} · {{ detail.order.customerName }} · {{ detail.order.referenceId || '无客户参考号' }}</p>
</div>
<StatusBadge :status="statusLabel(detail.order.status)" />
</div>
<div class="detail-section">
<h3>订单概要</h3>
<el-descriptions :column="2" border>
<el-descriptions-item label="目标仓 / 国家">{{ detail.order.destinationWarehouseName || '-' }} · {{ detail.order.destinationCountryCode || '-' }}</el-descriptions-item>
<el-descriptions-item label="平台收货仓">{{ detail.order.receivingWarehouseName || '-' }}</el-descriptions-item>
<el-descriptions-item label="收货地址" :span="2">{{ detail.order.receivingWarehouseAddress || '-' }}</el-descriptions-item>
<el-descriptions-item label="渠道">{{ detail.order.channelName || '-' }}({{ detail.order.channelCode || '-' }})</el-descriptions-item>
<el-descriptions-item label="预计出运">{{ detail.order.expectedShipDate || '-' }}</el-descriptions-item>
<el-descriptions-item label="客户备注" :span="2">{{ detail.order.customerRemark || '-' }}</el-descriptions-item>
</el-descriptions>
</div>
<div class="detail-section">
<h3>货物明细</h3>
<div class="detail-table-scroll">
<el-table :data="detail.cargoes" size="small" empty-text="暂无货物明细">
<el-table-column prop="itemNo" label="#" width="52" />
<el-table-column prop="cargoName" label="品名" min-width="150" />
<el-table-column prop="packageType" label="包装" min-width="100" />
<el-table-column prop="packageCount" label="件/箱" width="86" />
<el-table-column label="毛重 / 体积" min-width="150"><template #default="{ row }">{{ row.grossWeightKg }} kg / {{ row.volumeCbm }} m³</template></el-table-column>
</el-table>
</div>
</div>
<div class="detail-section">
<h3>收发货方</h3>
<div class="party-grid">
<div v-for="party in detail.parties" :key="party.id">
<span>{{ party.partyType === 'SENDER' ? '发货方' : '收货方' }}</span>
<strong>{{ party.companyName }}</strong>
<p>{{ party.contactName }} · {{ party.phone }}</p>
<p>{{ [party.province, party.city, party.district, party.addressLine].filter(Boolean).join(' ') }}</p>
</div>
</div>
</div>
<div class="detail-section">
<h3>履约快递</h3>
<el-descriptions :column="2" border>
<el-descriptions-item label="快递公司">{{ detail.order.expressCompany || '-' }}</el-descriptions-item>
<el-descriptions-item label="快递单号">{{ detail.order.expressTrackingNo || '-' }}</el-descriptions-item>
</el-descriptions>
</div>
<div class="detail-section">
<h3>内部备注</h3>
<el-empty v-if="!detail.internalNotes.length" description="暂无内部备注" :image-size="52" />
<el-timeline v-else><el-timeline-item v-for="note in detail.internalNotes" :key="note.id" :timestamp="formattedDate(note.createdAt)" placement="top"><p>{{ note.content }}</p></el-timeline-item></el-timeline>
</div>
<div class="detail-section">
<h3>状态历史</h3>
<el-timeline><el-timeline-item v-for="item in detail.statusHistory" :key="item.id" :timestamp="`${formattedDate(item.occurredAt)} · ${item.operatorName || '系统'}`" placement="top"><strong>{{ statusLabel(item.toStatus) }}</strong><p>{{ item.reason || item.action || '状态已更新。' }}</p></el-timeline-item></el-timeline>
</div>
</template>
<el-empty v-else-if="!detailLoading" description="订单详情暂不可用"><el-button type="primary" @click="closeDetailPage"><ArrowLeft :size="16" />返回订单列表</el-button></el-empty>
</DetailPageLayout>
</section>
</template>
<template v-else>
<ListPageLayout ariaLabel="头程订单列表">
<template #toolbar>
<QueryToolbar
v-model="filters"
ariaLabel="头程订单筛选"
:fields="queryFields"
preference-key="first-leg-orders-query"
search-text="查询"
:search-on-reset="false"
@search="searchOrders"
@reset="resetOrders"
/>
</template>
<el-alert v-if="tenantLoadError" type="warning" :closable="false" show-icon role="alert">
<template #title><div class="api-error-alert-title"><span>{{ tenantLoadError.message }}</span><ApiErrorMeta :details="tenantLoadError" /></div></template>
<el-button link type="warning" @click="loadTenants">重新加载企业列表</el-button>
</el-alert>
<template v-if="loadError" #state>
<el-alert type="error" :closable="false" show-icon role="alert">
<template #title><div class="api-error-alert-title"><span>{{ loadError.message }}</span><ApiErrorMeta :details="loadError" /></div></template>
<el-button link type="danger" @click="loadOrders">重新加载</el-button>
</el-alert>
</template>
<section v-if="!loadError" class="surface-panel table-panel">
<DataTable
v-model:page="page"
v-model:page-size="pageSize"
:data="orders"
:columns="orderColumns"
preference-key="first-leg-orders-table"
:loading="loading"
:total="total"
:page-sizes="[20, 50, 100]"
empty-text="当前没有符合条件的头程订单"
row-key="id"
>
<template #pagination-left="{ total: tableTotal }"><span>共 {{ tableTotal }} 条订单</span></template>
<template #actions="{ row }">
<div class="table-actions order-row-actions">
<el-button link type="primary" :disabled="actionLoadingId === String((row as PlatformFirstLegOrderResource).id)" @click="openDetail(row as PlatformFirstLegOrderResource)"><Eye :size="15" />详情</el-button>
<el-button v-if="actionLabel(row as PlatformFirstLegOrderResource)" link type="success" :loading="actionLoadingId === String((row as PlatformFirstLegOrderResource).id)" @click="runLifecycleAction(row as PlatformFirstLegOrderResource)">{{ actionLabel(row as PlatformFirstLegOrderResource) }}</el-button>
<el-dropdown v-if="hasSecondaryActions(row as PlatformFirstLegOrderResource)" trigger="click" popper-class="first-leg-action-menu" :disabled="actionLoadingId === String((row as PlatformFirstLegOrderResource).id)" @command="runSecondaryAction($event, row as PlatformFirstLegOrderResource)">
<el-button link type="primary" aria-label="更多订单操作" title="更多订单操作"><MoreHorizontal :size="17" /></el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item v-if="canMaintainExpress(row as PlatformFirstLegOrderResource)" command="express"><PackageCheck :size="15" />维护快递</el-dropdown-item>
<el-dropdown-item v-if="can('platform:order:note')" command="note"><FilePenLine :size="15" />内部备注</el-dropdown-item>
<el-dropdown-item v-if="canCancel(row as PlatformFirstLegOrderResource)" command="cancel" divided><CircleX :size="15" />取消订单</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</template>
<template #status="{ row }"><StatusBadge :status="statusLabel((row as PlatformFirstLegOrderResource).status)" /></template>
<template #orderInfo="{ row }"><div class="primary-cell"><strong>{{ (row as PlatformFirstLegOrderResource).orderNo || '-' }}</strong><span>{{ (row as PlatformFirstLegOrderResource).referenceId || '无客户参考号' }} · {{ formattedDate((row as PlatformFirstLegOrderResource).createdAt) }}</span></div></template>
<template #customer="{ row }"><div class="primary-cell"><strong>{{ customerDisplay(row as PlatformFirstLegOrderResource).name }}</strong><span v-if="customerDisplay(row as PlatformFirstLegOrderResource).secondary">{{ customerDisplay(row as PlatformFirstLegOrderResource).secondary }}</span></div></template>
<template #transport="{ row }"><div class="primary-cell"><strong>{{ transportLabel(row as PlatformFirstLegOrderResource) }}</strong><span>{{ (row as PlatformFirstLegOrderResource).channelName || '未填写渠道' }}</span></div></template>
<template #destination="{ row }"><div class="primary-cell"><strong>{{ (row as PlatformFirstLegOrderResource).destinationWarehouseName || '-' }}</strong><span>{{ (row as PlatformFirstLegOrderResource).destinationCountryCode || '-' }}</span></div></template>
<template #receivingWarehouse="{ row }"><div class="primary-cell"><strong>{{ (row as PlatformFirstLegOrderResource).receivingWarehouseName || '-' }}</strong><span>{{ (row as PlatformFirstLegOrderResource).receivingWarehouseAddress || '-' }}</span></div></template>
<template #express="{ row }"><div class="primary-cell"><strong>{{ (row as PlatformFirstLegOrderResource).expressCompany || '未维护' }}</strong><span>{{ (row as PlatformFirstLegOrderResource).expressTrackingNo || '—' }}</span></div></template>
</DataTable>
</section>
</ListPageLayout>
</template>
<el-dialog v-model="expressDialogOpen" title="维护履约快递" width="min(520px, calc(100vw - 28px))">
<el-alert title="当前后端契约只接收快递公司名称和快递单号;快递主数据关联待后端补充。" type="info" :closable="false" show-icon />
<el-form ref="expressFormRef" :model="expressForm" :rules="expressRules" label-position="top" class="delivery-form"><el-form-item label="快递公司" prop="expressCompany" required><el-input v-model.trim="expressForm.expressCompany" maxlength="64" placeholder="例如 顺丰速运" /></el-form-item><el-form-item label="快递单号" prop="expressTrackingNo" required><el-input v-model.trim="expressForm.expressTrackingNo" maxlength="128" placeholder="请输入完整快递单号" /></el-form-item></el-form>
<template #footer><el-button :disabled="expressSaving" @click="expressDialogOpen = false">取消</el-button><el-button type="primary" :loading="expressSaving" @click="saveExpress">保存快递信息</el-button></template>
</el-dialog>
<el-dialog v-model="noteDialogOpen" title="添加内部备注" width="min(520px, calc(100vw - 28px))">
<el-form ref="noteFormRef" :model="noteForm" :rules="noteRules" label-position="top"><el-form-item label="内部备注" prop="content" required><el-input v-model.trim="noteForm.content" maxlength="2000" type="textarea" :rows="4" show-word-limit placeholder="仅平台内部可见" /></el-form-item></el-form>
<template #footer><el-button :disabled="noteSaving" @click="noteDialogOpen = false">取消</el-button><el-button type="primary" :loading="noteSaving" @click="saveNote">保存备注</el-button></template>
</el-dialog>
</div>
</AdminPage>
</template>
<style scoped>
.detail-page { overflow: hidden; }
.detail-page-header { display: flex; min-height: 88px; align-items: center; justify-content: space-between; gap: 18px; padding: 20px 24px; border-bottom: 1px solid var(--cc-border); }
.detail-page-header h1 { margin: 0; font-size: 20px; line-height: 1.35; }
.detail-page-header p { margin: 5px 0 0; color: var(--cc-muted); font-size: 12px; line-height: 1.5; }
.detail-table-scroll { overflow-x: auto; }
.party-grid p { margin: 4px 0 0; color: var(--cc-muted); font-size: 12px; line-height: 1.6; overflow-wrap: anywhere; }
.order-row-actions { gap: 8px; white-space: nowrap; }
.order-row-actions :deep(.el-button + .el-button) { margin-left: 0; }
:global(.first-leg-action-menu .el-dropdown-menu__item) { min-height: 44px; gap: 8px; }
@media (max-width: 720px) {
.detail-page-header { align-items: flex-start; flex-direction: column; padding: 16px; }
.detail-page-header .el-button { width: 100%; min-height: 40px; }
.detail-table-scroll :deep(.el-table) { min-width: 560px; }
}
</style>