优化
This commit is contained in:
@@ -47,6 +47,9 @@ describe('UnifiedOrdersView contract', () => {
|
||||
|
||||
it('covers real list states and keeps operation column first and left-fixed', () => {
|
||||
expect(source).toContain("{ key: 'actions', label: '操作', width: 180, fixed: 'left'")
|
||||
expect(source).toContain("{ key: 'selection', type: 'selection', width: 52, fixed: 'left'")
|
||||
expect(source).toContain('批量审核接口待接入')
|
||||
expect(source).toContain('@selection-change="handleSelectionChange"')
|
||||
expect(source).toContain('title="订单列表暂时无法加载"')
|
||||
expect(source).toContain('empty-text="暂无符合条件的订单"')
|
||||
expect(source).toContain(':loading="loading"')
|
||||
@@ -84,6 +87,16 @@ describe('UnifiedOrdersView contract', () => {
|
||||
expect(source).toContain('订单详情暂时无法加载')
|
||||
})
|
||||
|
||||
it('renders order detail as a routed full page instead of a drawer', () => {
|
||||
expect(source).toContain('<template v-if="orderId">')
|
||||
expect(source).toContain('class="order-detail-header"')
|
||||
expect(source).toContain('aria-label="订单详情模块"')
|
||||
expect(source).toContain("{ key: 'basic', label: '基本信息'")
|
||||
expect(source).toContain("{ key: 'measurement', label: '测量数据'")
|
||||
expect(source).not.toContain('<AppDrawer')
|
||||
expect(source).not.toContain('detailOpen')
|
||||
})
|
||||
|
||||
it('exposes the C06 workflow entry only for inbound-backed downstream states', () => {
|
||||
expect(source).toContain("name: 'unified-order-workflow'")
|
||||
expect(source).toContain("['WAIT_SPLIT', 'LOADING', 'LOADED'].includes(row.mainStatus)")
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
|
||||
import { ChevronDown, ChevronUp, CircleCheck, ClipboardList, Eye, RefreshCw, SlidersHorizontal } from '@lucide/vue'
|
||||
import { ArrowLeft, Box, ChevronDown, ChevronUp, CircleCheck, ClipboardList, Eye, History, ListChecks, PackageOpen, RefreshCw, Ruler, SlidersHorizontal, StickyNote, WalletCards } from '@lucide/vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import {
|
||||
getPlatformUnifiedOrder,
|
||||
approvePlatformUnifiedOrder,
|
||||
assignPlatformUnifiedOrderCarrier,
|
||||
addPlatformUnifiedOrderNote,
|
||||
listPlatformUnifiedOrders,
|
||||
unifiedOrderStatusOptions,
|
||||
type UnifiedOrderDetailResource,
|
||||
@@ -20,7 +21,6 @@ import { listAllPlatformWarehouses, type PlatformWarehouseSummaryResource } from
|
||||
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
||||
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
||||
import AppDialog from '@/shared/components/AppDialog.vue'
|
||||
import AppDrawer from '@/shared/components/AppDrawer.vue'
|
||||
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
|
||||
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
|
||||
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
|
||||
@@ -52,6 +52,7 @@ const route = useRoute()
|
||||
const router = useRouter()
|
||||
const access = useAccessPreviewStore()
|
||||
const records = ref<UnifiedOrderResource[]>([])
|
||||
const selectedOrders = ref<UnifiedOrderResource[]>([])
|
||||
const customers = ref<PlatformTenantResource[]>([])
|
||||
const warehouses = ref<PlatformWarehouseSummaryResource[]>([])
|
||||
const page = ref(1)
|
||||
@@ -66,7 +67,7 @@ const warehouseLoadError = ref<ApiErrorDetails | null>(null)
|
||||
const detail = ref<UnifiedOrderDetailResource | null>(null)
|
||||
const detailLoading = ref(false)
|
||||
const detailError = ref<ApiErrorDetails | null>(null)
|
||||
const detailOpen = ref(false)
|
||||
const activeDetailTab = ref('basic')
|
||||
const approvalOpen = ref(false)
|
||||
const approvalLoadingId = ref('')
|
||||
const approvalSaving = ref(false)
|
||||
@@ -76,6 +77,9 @@ const carriers = ref<PlatformPartnerResource[]>([])
|
||||
const approvalOrder = ref<UnifiedOrderResource | null>(null)
|
||||
const carrierFormRef = ref<FormInstance>()
|
||||
const carrierForm = reactive({ carrierId: '' })
|
||||
const noteOpen = ref(false)
|
||||
const noteSaving = ref(false)
|
||||
const noteContent = ref('')
|
||||
const showAdvancedFilters = ref(false)
|
||||
const filters = ref<FilterModel>(emptyFilters())
|
||||
const carrierRules: FormRules = {
|
||||
@@ -84,6 +88,15 @@ const carrierRules: FormRules = {
|
||||
|
||||
const orderId = computed(() => typeof route.params.orderId === 'string' ? route.params.orderId : '')
|
||||
|
||||
const detailTabs = [
|
||||
{ key: 'basic', label: '基本信息', icon: Box },
|
||||
{ key: 'cargo', label: '货品信息', icon: PackageOpen },
|
||||
{ key: 'fees', label: '费用信息', icon: WalletCards },
|
||||
{ key: 'history', label: '状态历史', icon: History },
|
||||
{ key: 'measurement', label: '测量数据', icon: Ruler },
|
||||
{ key: 'other', label: '其他', icon: StickyNote },
|
||||
]
|
||||
|
||||
const statusTabs = computed(() => [
|
||||
{ value: '' as const, label: '全部' },
|
||||
...unifiedOrderStatusOptions,
|
||||
@@ -141,21 +154,66 @@ const advancedQueryFields = computed<QueryField[]>(() => [
|
||||
{ key: 'fbaCode', type: 'input', width: 220, label: '商品 FBA 编码', placeholder: '请输入商品 FBA 编码', props: { 'aria-label': '商品 FBA 编码' } },
|
||||
])
|
||||
|
||||
const allQueryFields = computed(() => [...primaryQueryFields.value, ...advancedQueryFields.value])
|
||||
const queryFields = computed(() => showAdvancedFilters.value
|
||||
? [...primaryQueryFields.value, ...advancedQueryFields.value]
|
||||
? allQueryFields.value
|
||||
: primaryQueryFields.value)
|
||||
|
||||
const columns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', width: 180, fixed: 'left', slotName: 'actions' },
|
||||
{ key: 'order', label: '订单信息', minWidth: 220, slotName: 'order' },
|
||||
{ key: 'mainStatus', label: '主状态', width: 132, slotName: 'mainStatus' },
|
||||
{ key: 'customer', label: '客户', minWidth: 180, slotName: 'customer' },
|
||||
{ key: 'receivingWarehouseName', label: '平台收货仓', minWidth: 160, slotName: 'receivingWarehouseName' },
|
||||
{ key: 'destinationWarehouseName', label: '目的仓', minWidth: 160, slotName: 'destinationWarehouseName' },
|
||||
{ key: 'transport', label: '运输/装柜', minWidth: 180, slotName: 'transport' },
|
||||
{ key: 'cargo', label: '货物数据', minWidth: 180, slotName: 'cargo' },
|
||||
{ key: 'expectedShipDate', label: '预计出运', minWidth: 150, slotName: 'expectedShipDate' },
|
||||
{ key: 'updatedAt', label: '最近更新', minWidth: 150, slotName: 'updatedAt' },
|
||||
{ key: 'selection', type: 'selection', width: 52, fixed: 'left', align: 'center', selectable: canBulkReview },
|
||||
{ key: 'inboundCode', prop: 'inboundCode', label: '入仓码', minWidth: 140, slotName: 'field' },
|
||||
{ key: 'internalOrderStatus', prop: 'internalOrderStatus', label: '订单状态', minWidth: 130, slotName: 'field' },
|
||||
{ key: 'measurementReviewStatus', prop: 'measurementReviewStatus', label: '测量数据审核状态', minWidth: 170, slotName: 'field' },
|
||||
{ key: 'expectedArrivalAt', prop: 'expectedArrivalAt', label: '预计到达时间', minWidth: 150, slotName: 'field' },
|
||||
{ key: 'feeReviewStatus', prop: 'feeReviewStatus', label: '费用审核状态', minWidth: 140, slotName: 'field' },
|
||||
{ key: 'customerName', prop: 'customerName', label: '客户', minWidth: 150, slotName: 'field' },
|
||||
{ key: 'receivingWarehouseName', prop: 'receivingWarehouseName', label: '仓库', minWidth: 150, slotName: 'field' },
|
||||
{ key: 'destinationWarehouseName', prop: 'destinationWarehouseName', label: '交货仓', minWidth: 150, slotName: 'field' },
|
||||
{ key: 'loadingStatus', prop: 'loadingStatus', label: '配载状态', minWidth: 130, slotName: 'field' },
|
||||
{ key: 'holdCargo', prop: 'holdCargo', label: '是否扣货', minWidth: 110, slotName: 'field' },
|
||||
{ key: 'holdReason', prop: 'holdReason', label: '扣货理由', minWidth: 160, slotName: 'field' },
|
||||
{ key: 'containerNo', prop: 'containerNo', label: '集装箱号', minWidth: 150, slotName: 'field' },
|
||||
{ key: 'cabinetNo', prop: 'cabinetNo', label: '柜号', minWidth: 130, slotName: 'field' },
|
||||
{ key: 'companyName', prop: 'companyName', label: '公司', minWidth: 150, slotName: 'field' },
|
||||
{ key: 'businessType', prop: 'businessType', label: '业务类型', minWidth: 130, slotName: 'field' },
|
||||
{ key: 'pickupMethod', prop: 'pickupMethod', label: '提货方式', minWidth: 130, slotName: 'field' },
|
||||
{ key: 'pickupAt', prop: 'pickupAt', label: '提货时间', minWidth: 150, slotName: 'field' },
|
||||
{ key: 'pickupContactPhone', prop: 'pickupContactPhone', label: '提货联系人电话', minWidth: 150, slotName: 'field' },
|
||||
{ key: 'pickupAddress', prop: 'pickupAddress', label: '提货地址', minWidth: 200, slotName: 'field' },
|
||||
{ key: 'pickupPostalCode', prop: 'pickupPostalCode', label: '提货邮编', minWidth: 110, slotName: 'field' },
|
||||
{ key: 'pickupContactEmail', prop: 'pickupContactEmail', label: '提货联系人邮箱', minWidth: 190, slotName: 'field' },
|
||||
{ key: 'channelName', prop: 'channelName', label: '渠道', minWidth: 130, slotName: 'field' },
|
||||
{ key: 'taxMode', prop: 'taxMode', label: '买单/退税', minWidth: 130, slotName: 'field' },
|
||||
{ key: 'fullContainer', prop: 'fullContainer', label: '是否整柜', minWidth: 110, slotName: 'field' },
|
||||
{ key: 'customsInspection', prop: 'customsInspection', label: '商检验', minWidth: 110, slotName: 'field' },
|
||||
{ key: 'sailingAt', prop: 'sailingAt', label: '开船时间', minWidth: 150, slotName: 'field' },
|
||||
{ key: 'portArrivalAt', prop: 'portArrivalAt', label: '到港时间', minWidth: 150, slotName: 'field' },
|
||||
{ key: 'totalPackageCount', prop: 'totalPackageCount', label: '总箱数', minWidth: 100, slotName: 'field' },
|
||||
{ key: 'totalGrossWeightKg', prop: 'totalGrossWeightKg', label: '总毛重(T.K.GW)', minWidth: 140, slotName: 'field' },
|
||||
{ key: 'chargeableWeightKg', prop: 'chargeableWeightKg', label: '计费毛重', minWidth: 120, slotName: 'field' },
|
||||
{ key: 'totalVolumeCbm', prop: 'totalVolumeCbm', label: '总体积(m³)', minWidth: 120, slotName: 'field' },
|
||||
{ key: 'chargeableVolumeCbm', prop: 'chargeableVolumeCbm', label: '计费体积(m³)', minWidth: 130, slotName: 'field' },
|
||||
{ key: 'totalPalletCount', prop: 'totalPalletCount', label: '总托盘数', minWidth: 110, slotName: 'field' },
|
||||
{ key: 'location', prop: 'location', label: '库位位置', minWidth: 130, slotName: 'field' },
|
||||
{ key: 'volumetricWeightKg', prop: 'volumetricWeightKg', label: '总材积重', minWidth: 120, slotName: 'field' },
|
||||
{ key: 'totalNetWeightKg', prop: 'totalNetWeightKg', label: '总净重(T.K.NW)', minWidth: 140, slotName: 'field' },
|
||||
{ key: 'destinationType', prop: 'destinationType', label: '目的仓类型', minWidth: 130, slotName: 'field' },
|
||||
{ key: 'destinationAddress', prop: 'destinationAddress', label: '地址', minWidth: 200, slotName: 'field' },
|
||||
{ key: 'destinationPostalCode', prop: 'destinationPostalCode', label: '邮编', minWidth: 100, slotName: 'field' },
|
||||
{ key: 'destinationContact', prop: 'destinationContact', label: '联系人', minWidth: 120, slotName: 'field' },
|
||||
{ key: 'destinationContactPhone', prop: 'destinationContactPhone', label: '联系人电话', minWidth: 140, slotName: 'field' },
|
||||
{ key: 'measurementCompletedAt', prop: 'measurementCompletedAt', label: '测量完成时间', minWidth: 150, slotName: 'field' },
|
||||
{ key: 'inboundCompletedAt', prop: 'inboundCompletedAt', label: '入库完成时间', minWidth: 150, slotName: 'field' },
|
||||
{ key: 'associatedFiles', prop: 'associatedFiles', label: '关联文件', minWidth: 120, slotName: 'field' },
|
||||
{ key: 'markFiles', prop: 'markFiles', label: '唛头文件', minWidth: 120, slotName: 'field' },
|
||||
{ key: 'pcOrderFiles', prop: 'pcOrderFiles', label: 'PC端下单文件', minWidth: 140, slotName: 'field' },
|
||||
{ key: 'expressCompany', prop: 'expressCompany', label: '快递公司', minWidth: 130, slotName: 'field' },
|
||||
{ key: 'expressTrackingNo', prop: 'expressTrackingNo', label: '快递单号', minWidth: 150, slotName: 'field' },
|
||||
{ key: 'remark', prop: 'remark', label: '备注', minWidth: 180, slotName: 'field' },
|
||||
{ key: 'internalRemark', prop: 'internalRemark', label: '客服内部备注', minWidth: 180, slotName: 'field' },
|
||||
{ key: 'createdBy', prop: 'createdBy', label: '创建人', minWidth: 120, slotName: 'field' },
|
||||
{ key: 'createdAt', prop: 'createdAt', label: '创建日期', minWidth: 150, slotName: 'field' },
|
||||
]
|
||||
|
||||
function emptyFilters(): FilterModel {
|
||||
@@ -211,10 +269,42 @@ function formatNumber(value: number | null | undefined, digits = 2) {
|
||||
return value === null || value === undefined ? '—' : value.toLocaleString('zh-CN', { maximumFractionDigits: digits })
|
||||
}
|
||||
|
||||
function displayField(row: UnifiedOrderResource, property: string | undefined) {
|
||||
if (!property) return '—'
|
||||
const source = row as unknown as Record<string, unknown>
|
||||
const nested: Record<string, string> = {
|
||||
measurementReviewStatus: 'measurement.reviewStatus', expectedArrivalAt: 'expectedArrivalAt',
|
||||
loadingStatus: 'transport.loadingStatus', containerNo: 'transport.containerNo', cabinetNo: 'transport.cabinetNo',
|
||||
sailingAt: 'transport.sailingAt', portArrivalAt: 'transport.portArrivalAt',
|
||||
chargeableWeightKg: 'measurement.chargeableWeightKg', measurementCompletedAt: 'measurement.completedAt',
|
||||
totalGrossWeightKg: 'measurement.actualGrossWeightKg', totalVolumeCbm: 'measurement.actualVolumeCbm',
|
||||
expressTrackingNo: 'delivery.trackingNo',
|
||||
}
|
||||
const path = nested[property] || property
|
||||
let value = path.split('.').reduce<unknown>((current, key) => (
|
||||
current && typeof current === 'object' ? (current as Record<string, unknown>)[key] : undefined
|
||||
), source)
|
||||
if ((value === null || value === undefined || value === '') && property === 'internalOrderStatus') value = source.mainStatus
|
||||
if (value === null || value === undefined || value === '') return '—'
|
||||
if (typeof value === 'boolean') return value ? '是' : '否'
|
||||
if (Array.isArray(value)) return value.length ? `${value.length} 个文件` : '—'
|
||||
if (typeof value === 'number') return formatNumber(value)
|
||||
if (property.endsWith('At') || property.endsWith('Date')) return formatDate(String(value))
|
||||
if (property === 'measurementReviewStatus') return measurementStatusLabel(String(value))
|
||||
if (property === 'internalOrderStatus') {
|
||||
return ({ SUBMITTED: '待审核', APPROVED: '已审核', ACCEPTED: '已受理', PROCESSING: '处理中', COMPLETED: '已完成', CANCELLED: '已取消' } as Record<string, string>)[String(value)] || String(value)
|
||||
}
|
||||
return String(value)
|
||||
}
|
||||
|
||||
function statusLabel(value: UnifiedOrderMainStatus | string | null) {
|
||||
return unifiedOrderStatusOptions.find((item) => item.value === value)?.label || value || '状态更新'
|
||||
}
|
||||
|
||||
function measurementStatusLabel(value: string | null | undefined) {
|
||||
return ({ UNRECORDED: '未录入', PENDING: '待审核', APPROVED: '审核通过', REJECTED: '审核驳回' } as Record<string, string>)[value || ''] || value || '未录入'
|
||||
}
|
||||
|
||||
const nextActionLabels: Record<string, string> = {
|
||||
APPROVE_ORDER: '审核通过',
|
||||
CONFIRM_MEASUREMENT: '完成测量',
|
||||
@@ -234,6 +324,19 @@ function canApprove(row: UnifiedOrderResource) {
|
||||
return row.mainStatus === 'PENDING_REVIEW' && row.nextAction === 'APPROVE_ORDER' && can('platform:order:approve')
|
||||
}
|
||||
|
||||
function canBulkReview(row: UnifiedOrderResource) {
|
||||
return canApprove(row)
|
||||
}
|
||||
|
||||
function handleSelectionChange(rows: UnifiedOrderResource[]) {
|
||||
selectedOrders.value = rows
|
||||
}
|
||||
|
||||
function openBulkReviewPlaceholder() {
|
||||
if (selectedOrders.value.length === 0) return
|
||||
ElMessage.info('批量审核接口待接入,请先逐笔审核')
|
||||
}
|
||||
|
||||
async function loadCarriers() {
|
||||
carrierLoading.value = true
|
||||
carrierLoadError.value = null
|
||||
@@ -304,6 +407,31 @@ async function assignCarrierAndApprove() {
|
||||
}
|
||||
}
|
||||
|
||||
function openNoteDialog() {
|
||||
if (!detail.value || !can('platform:order:note')) return
|
||||
noteContent.value = ''
|
||||
noteOpen.value = true
|
||||
}
|
||||
|
||||
async function saveNote() {
|
||||
const content = noteContent.value.trim()
|
||||
if (!detail.value || !content) {
|
||||
ElMessage.warning('请填写内部备注')
|
||||
return
|
||||
}
|
||||
noteSaving.value = true
|
||||
try {
|
||||
await addPlatformUnifiedOrderNote(String(detail.value.summary.tenantId), String(detail.value.summary.orderId), content)
|
||||
noteOpen.value = false
|
||||
await loadDetail()
|
||||
ElMessage.success('内部备注已保存')
|
||||
} catch (error) {
|
||||
ElMessage.error(getApiErrorDetails(error, '内部备注保存失败,请稍后重试').message)
|
||||
} finally {
|
||||
noteSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadCustomers() {
|
||||
customerLoading.value = true
|
||||
customerLoadError.value = null
|
||||
@@ -369,6 +497,7 @@ async function loadOrders() {
|
||||
size: pageSize.value,
|
||||
})
|
||||
records.value = result.records
|
||||
selectedOrders.value = []
|
||||
total.value = result.total
|
||||
} catch (error) {
|
||||
records.value = []
|
||||
@@ -393,8 +522,10 @@ function reset() {
|
||||
void loadOrders()
|
||||
}
|
||||
|
||||
function openDetail(row: UnifiedOrderResource) {
|
||||
void router.push({ name: 'unified-order-detail', params: { orderId: String(row.orderId) }, query: { tenantId: String(row.tenantId) } })
|
||||
async function openDetail(row: UnifiedOrderResource) {
|
||||
await router.push({ name: 'unified-order-detail', params: { orderId: String(row.orderId) }, query: { tenantId: String(row.tenantId) } })
|
||||
await nextTick()
|
||||
window.scrollTo({ top: 0 })
|
||||
}
|
||||
|
||||
function openMeasurement(row: UnifiedOrderResource) {
|
||||
@@ -416,7 +547,6 @@ function openWorkflow(row: UnifiedOrderResource) {
|
||||
}
|
||||
|
||||
function closeDetail() {
|
||||
detailOpen.value = false
|
||||
detail.value = null
|
||||
detailError.value = null
|
||||
void router.push({ name: 'unified-orders' })
|
||||
@@ -427,12 +557,10 @@ async function loadDetail() {
|
||||
const tenantId = typeof route.query.tenantId === 'string' ? route.query.tenantId : ''
|
||||
if (!tenantId) {
|
||||
detailError.value = { message: '订单详情缺少客户标识,请从订单列表重新打开' }
|
||||
detailOpen.value = true
|
||||
return
|
||||
}
|
||||
detailLoading.value = true
|
||||
detailError.value = null
|
||||
detailOpen.value = true
|
||||
try {
|
||||
detail.value = await getPlatformUnifiedOrder(tenantId, orderId.value)
|
||||
} catch (error) {
|
||||
@@ -445,8 +573,10 @@ async function loadDetail() {
|
||||
|
||||
watch([page, pageSize], () => { void loadOrders() })
|
||||
watch(orderId, (value) => {
|
||||
if (value) void loadDetail()
|
||||
else detailOpen.value = false
|
||||
if (value) {
|
||||
activeDetailTab.value = 'basic'
|
||||
void loadDetail()
|
||||
}
|
||||
}, { immediate: true })
|
||||
onMounted(() => {
|
||||
void loadCustomers()
|
||||
@@ -457,35 +587,132 @@ onMounted(() => {
|
||||
|
||||
<template>
|
||||
<section class="unified-orders-page">
|
||||
<div class="status-tabs" role="tablist" aria-label="订单主状态">
|
||||
<button
|
||||
v-for="(tab, index) in statusTabs"
|
||||
:key="tab.value || 'ALL'"
|
||||
type="button"
|
||||
role="tab"
|
||||
:aria-selected="filters.mainStatus === tab.value"
|
||||
aria-controls="unified-orders-results"
|
||||
:tabindex="filters.mainStatus === tab.value ? 0 : -1"
|
||||
:class="['status-tab', { 'is-active': filters.mainStatus === tab.value }]"
|
||||
@click="selectStatus(tab.value)"
|
||||
@keydown="handleTabKeydown($event, index)"
|
||||
>{{ tab.label }}</button>
|
||||
</div>
|
||||
<template v-if="orderId">
|
||||
<header class="order-detail-header">
|
||||
<div class="order-detail-heading">
|
||||
<el-button class="back-button" @click="closeDetail"><ArrowLeft :size="17" />返回订单列表</el-button>
|
||||
<span class="order-mark" aria-hidden="true"><Box :size="23" /></span>
|
||||
<div class="order-title">
|
||||
<span>入仓码</span>
|
||||
<h1>{{ detail?.summary.inboundCode || '暂未生成' }}</h1>
|
||||
<p>{{ detail?.summary.orderNo || `订单 ${orderId}` }}</p>
|
||||
</div>
|
||||
<StatusBadge v-if="detail" :status="statusLabel(detail.summary.mainStatus)" />
|
||||
</div>
|
||||
<div v-if="detail" class="detail-header-actions">
|
||||
<el-button v-if="canApprove(detail.summary)" type="success" plain :loading="approvalLoadingId === String(detail.summary.orderId)" @click="openApproval(detail.summary)"><CircleCheck :size="15" />审核通过</el-button>
|
||||
<el-button v-if="workflowEligible(detail.summary) && can('platform:firstleg:inbound:view')" type="primary" @click="openWorkflow(detail.summary)"><ClipboardList :size="15" />后半程作业</el-button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<QueryToolbar id="unified-orders-advanced-filters" v-model="filters" ariaLabel="订单列表筛选" :fields="queryFields" :search-on-reset="false" @search="search" @reset="reset">
|
||||
<template #actions>
|
||||
<el-button :aria-expanded="showAdvancedFilters" aria-controls="unified-orders-advanced-filters" @click="showAdvancedFilters = !showAdvancedFilters">
|
||||
<SlidersHorizontal :size="16" />{{ showAdvancedFilters ? '收起筛选' : '高级筛选' }}
|
||||
<ChevronUp v-if="showAdvancedFilters" :size="15" /><ChevronDown v-else :size="15" />
|
||||
</el-button>
|
||||
</template>
|
||||
</QueryToolbar>
|
||||
<LoadErrorState v-if="detailError" class="detail-load-error" title="订单详情暂时无法加载" :message="detailError.message" :loading="detailLoading" @retry="loadDetail">
|
||||
<template #details><ApiErrorMeta :details="detailError" /></template>
|
||||
</LoadErrorState>
|
||||
|
||||
<div v-else v-loading="detailLoading" class="order-detail-content">
|
||||
<template v-if="detail">
|
||||
<section class="detail-overview" aria-label="订单概览">
|
||||
<article><span>客户</span><strong>{{ customerName(detail.summary) }}</strong><small>{{ detail.summary.channelName || '未配置渠道' }}</small></article>
|
||||
<article><span>总箱数</span><strong>{{ detail.summary.totalPackageCount ?? '—' }}<small>箱</small></strong><small>{{ detail.order.cargoes.length }} 条货品明细</small></article>
|
||||
<article><span>实测毛重</span><strong>{{ formatNumber(detail.summary.measurement?.actualGrossWeightKg) }}<small>kg</small></strong><small>{{ measurementStatusLabel(detail.summary.measurement?.reviewStatus) }}</small></article>
|
||||
<article><span>实测体积</span><strong>{{ formatNumber(detail.summary.measurement?.actualVolumeCbm, 3) }}<small>m³</small></strong><small>最近更新 {{ formatDate(detail.summary.updatedAt) }}</small></article>
|
||||
</section>
|
||||
|
||||
<div class="detail-tabs" role="tablist" aria-label="订单详情模块">
|
||||
<button v-for="tab in detailTabs" :id="`order-detail-tab-${tab.key}`" :key="tab.key" type="button" role="tab" :class="{ active: activeDetailTab === tab.key }" :aria-selected="activeDetailTab === tab.key" :aria-controls="`order-detail-panel-${tab.key}`" :tabindex="activeDetailTab === tab.key ? 0 : -1" @click="activeDetailTab = tab.key"><component :is="tab.icon" :size="17" /><span>{{ tab.label }}</span></button>
|
||||
</div>
|
||||
|
||||
<section :id="`order-detail-panel-${activeDetailTab}`" class="detail-panel" role="tabpanel" :aria-labelledby="`order-detail-tab-${activeDetailTab}`" tabindex="0">
|
||||
<template v-if="activeDetailTab === 'basic'">
|
||||
<div class="panel-title"><div><h2>订单基本信息</h2><p>{{ detail.summary.orderNo || `订单 ${detail.summary.orderId}` }}</p></div><Box :size="19" /></div>
|
||||
<dl class="order-info-grid">
|
||||
<div><dt>公司</dt><dd>{{ customerName(detail.summary) }}</dd></div>
|
||||
<div><dt>运输方式</dt><dd>{{ detail.order.order?.transportMode || '—' }}</dd></div>
|
||||
<div><dt>当前状态</dt><dd><StatusBadge :status="statusLabel(detail.summary.mainStatus)" /></dd></div>
|
||||
<div><dt>平台收货仓</dt><dd>{{ detail.summary.receivingWarehouseName || '—' }}</dd></div>
|
||||
<div><dt>目的仓</dt><dd>{{ detail.summary.destinationWarehouseName || '—' }}</dd></div>
|
||||
<div><dt>目的国家</dt><dd>{{ detail.order.order?.destinationCountryCode || '—' }}</dd></div>
|
||||
<div><dt>渠道</dt><dd>{{ detail.summary.channelName || '—' }}</dd></div>
|
||||
<div><dt>承运商</dt><dd>{{ detail.order.order?.carrierName || '待平台指派' }}</dd></div>
|
||||
<div><dt>预计出运</dt><dd>{{ formatDate(detail.summary.expectedShipDate) }}</dd></div>
|
||||
<div><dt>集装箱号</dt><dd>{{ detail.summary.transport?.containerNo || '—' }}</dd></div>
|
||||
<div><dt>船名 / 航次</dt><dd>{{ detail.summary.transport?.vesselName || '—' }} / {{ detail.summary.transport?.voyageNo || '—' }}</dd></div>
|
||||
<div><dt>开船时间</dt><dd>{{ formatDate(detail.summary.transport?.sailingAt) }}</dd></div>
|
||||
<div><dt>到港时间</dt><dd>{{ formatDate(detail.summary.transport?.portArrivalAt) }}</dd></div>
|
||||
<div><dt>下一步</dt><dd>{{ canApprove(detail.summary) ? '选择承运商并审核' : nextActionBoundary(detail.summary) || '—' }}</dd></div>
|
||||
<div><dt>创建时间</dt><dd>{{ formatDate(detail.summary.createdAt) }}</dd></div>
|
||||
</dl>
|
||||
</template>
|
||||
|
||||
<template v-else-if="activeDetailTab === 'cargo'">
|
||||
<div class="panel-title"><div><h2>货品信息</h2><p>共 {{ detail.order.cargoes.length }} 条</p></div><PackageOpen :size="19" /></div>
|
||||
<div class="detail-table-wrap">
|
||||
<el-table :data="detail.order.cargoes" empty-text="暂无货品信息">
|
||||
<el-table-column prop="itemNo" label="#" width="64" /><el-table-column prop="cargoName" label="品名" min-width="170" /><el-table-column prop="packageType" label="包装" min-width="110" /><el-table-column prop="packageCount" label="箱数" width="90" /><el-table-column prop="grossWeightKg" label="毛重(kg)" min-width="110" /><el-table-column prop="volumeCbm" label="体积(m³)" min-width="110" />
|
||||
</el-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if="activeDetailTab === 'fees'">
|
||||
<div class="panel-title"><div><h2>费用信息</h2><p>订单费用与审核结果</p></div><WalletCards :size="19" /></div><el-empty description="暂无费用数据" :image-size="64" />
|
||||
</template>
|
||||
|
||||
<template v-else-if="activeDetailTab === 'history'">
|
||||
<div class="panel-title"><div><h2>状态历史</h2><p>共 {{ detail.timeline.length }} 条</p></div><History :size="19" /></div>
|
||||
<el-empty v-if="detail.timeline.length === 0" description="暂无状态历史" :image-size="64" />
|
||||
<ol v-else class="history-list" aria-label="状态历史"><li v-for="(item, index) in detail.timeline" :key="`${item.source || 'event'}-${item.occurredAt || index}`"><span class="history-marker" aria-hidden="true" /><div><div class="history-line"><strong>{{ item.action || item.eventType || '状态更新' }}</strong><StatusBadge :status="statusLabel(item.mainStatus)" /></div><p>{{ item.operatorName || '系统' }} · {{ formatDate(item.occurredAt) }}</p><p v-if="item.remark" class="history-remark">{{ item.remark }}</p></div></li></ol>
|
||||
</template>
|
||||
|
||||
<template v-else-if="activeDetailTab === 'measurement'">
|
||||
<div class="panel-title"><div><h2>测量数据</h2><p>{{ detail.summary.measurement?.completedAt ? `完成于 ${formatDate(detail.summary.measurement.completedAt)}` : '尚未完成测量' }}</p></div><Ruler :size="19" /></div>
|
||||
<dl class="measurement-grid"><div><dt>审核状态</dt><dd>{{ measurementStatusLabel(detail.summary.measurement?.reviewStatus) }}</dd></div><div><dt>实测毛重</dt><dd>{{ formatNumber(detail.summary.measurement?.actualGrossWeightKg) }} kg</dd></div><div><dt>实测体积</dt><dd>{{ formatNumber(detail.summary.measurement?.actualVolumeCbm, 3) }} m³</dd></div><div><dt>计费重量</dt><dd>{{ formatNumber(detail.summary.measurement?.chargeableWeightKg) }} kg</dd></div></dl>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div class="panel-title"><div><h2>内部备注</h2><p>仅平台内部可见</p></div><el-button v-if="can('platform:order:note')" type="primary" size="small" @click="openNoteDialog"><StickyNote :size="15" />添加备注</el-button></div>
|
||||
<el-empty v-if="detail.order.internalNotes.length === 0" description="暂无内部备注" :image-size="64" />
|
||||
<ul v-else class="notes-list" aria-label="内部备注"><li v-for="item in detail.order.internalNotes" :key="String(item.id)"><p>{{ item.content }}</p><span>操作人 {{ item.createdBy || '系统' }} · {{ formatDate(item.createdAt) }}</span></li></ul>
|
||||
</template>
|
||||
</section>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div class="orders-query-panel">
|
||||
<div class="status-tabs" role="tablist" aria-label="订单主状态">
|
||||
<button
|
||||
v-for="(tab, index) in statusTabs"
|
||||
:key="tab.value || 'ALL'"
|
||||
type="button"
|
||||
role="tab"
|
||||
:aria-selected="filters.mainStatus === tab.value"
|
||||
aria-controls="unified-orders-results"
|
||||
:tabindex="filters.mainStatus === tab.value ? 0 : -1"
|
||||
:class="['status-tab', { 'is-active': filters.mainStatus === tab.value }]"
|
||||
@click="selectStatus(tab.value)"
|
||||
@keydown="handleTabKeydown($event, index)"
|
||||
>{{ tab.label }}</button>
|
||||
</div>
|
||||
|
||||
<QueryToolbar id="unified-orders-advanced-filters" v-model="filters" ariaLabel="订单列表筛选" :fields="queryFields" :available-fields="allQueryFields" preference-key="unified-orders-query" :search-on-reset="false" @search="search" @reset="reset">
|
||||
<template #actions>
|
||||
<el-button :aria-expanded="showAdvancedFilters" aria-controls="unified-orders-advanced-filters" @click="showAdvancedFilters = !showAdvancedFilters">
|
||||
<SlidersHorizontal :size="16" />{{ showAdvancedFilters ? '收起筛选' : '高级筛选' }}
|
||||
<ChevronUp v-if="showAdvancedFilters" :size="15" /><ChevronDown v-else :size="15" />
|
||||
</el-button>
|
||||
</template>
|
||||
</QueryToolbar>
|
||||
</div>
|
||||
<LoadErrorState v-if="loadError" class="orders-load-error" title="订单列表暂时无法加载" :message="loadError.message" :loading="loading" @retry="loadOrders">
|
||||
<template #details><ApiErrorMeta :details="loadError" /></template>
|
||||
</LoadErrorState>
|
||||
|
||||
<div v-else id="unified-orders-results" class="table-card" role="tabpanel">
|
||||
<DataTable v-model:page="page" v-model:page-size="pageSize" :data="records" :columns="columns" :loading="loading" :total="total" :page-sizes="[20, 50, 100]" row-key="orderId" empty-text="暂无符合条件的订单" aria-label="统一订单列表">
|
||||
<DataTable v-model:page="page" v-model:page-size="pageSize" :data="records" :columns="columns" preference-key="unified-orders-table" :loading="loading" :total="total" :page-sizes="[20, 50, 100]" row-key="orderId" empty-text="暂无符合条件的订单" aria-label="统一订单列表" @selection-change="handleSelectionChange">
|
||||
<template #toolbar-start>
|
||||
<el-button type="primary" plain :disabled="selectedOrders.length === 0" title="批量审核接口待接入" @click="openBulkReviewPlaceholder"><ListChecks :size="16" />批量审核<span v-if="selectedOrders.length">({{ selectedOrders.length }})</span></el-button>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<div class="row-actions">
|
||||
<el-button link type="primary" @click="openDetail(row as UnifiedOrderResource)"><Eye :size="15" />详情</el-button>
|
||||
@@ -495,6 +722,7 @@ onMounted(() => {
|
||||
<span v-if="nextActionBoundary(row as UnifiedOrderResource)" class="action-boundary">{{ nextActionBoundary(row as UnifiedOrderResource) }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #field="{ row, column }">{{ displayField(row as UnifiedOrderResource, column.property) }}</template>
|
||||
<template #order="{ row }"><div class="primary-cell"><strong>{{ (row as UnifiedOrderResource).orderNo || `订单 ${(row as UnifiedOrderResource).orderId}` }}</strong><span v-if="(row as UnifiedOrderResource).inboundCode">入仓码 {{ (row as UnifiedOrderResource).inboundCode }}</span></div></template>
|
||||
<template #mainStatus="{ row }"><StatusBadge :status="statusLabel((row as UnifiedOrderResource).mainStatus)" /></template>
|
||||
<template #customer="{ row }"><div class="primary-cell"><strong>{{ customerName(row as UnifiedOrderResource) }}</strong><span>客户 ID {{ (row as UnifiedOrderResource).tenantId }}</span></div></template>
|
||||
@@ -507,17 +735,11 @@ onMounted(() => {
|
||||
</DataTable>
|
||||
</div>
|
||||
|
||||
<AppDrawer v-model="detailOpen" :title="detail ? `订单详情 · ${detail.summary.orderNo || detail.summary.orderId}` : '订单详情'" size="min(760px, 94vw)" :loading="detailLoading" :show-footer="false" destroy-on-close @closed="closeDetail">
|
||||
<LoadErrorState v-if="detailError" title="订单详情暂时无法加载" :message="detailError.message" :loading="detailLoading" @retry="loadDetail"><template #details><ApiErrorMeta :details="detailError" /></template></LoadErrorState>
|
||||
<div v-else-if="detail" class="detail-content">
|
||||
<section class="detail-summary" aria-label="订单概览">
|
||||
<div class="detail-summary__header"><div><span class="eyebrow">订单主状态</span><h2>{{ detail.summary.orderNo || `订单 ${detail.summary.orderId}` }}</h2></div><div class="detail-summary__actions"><StatusBadge :status="statusLabel(detail.summary.mainStatus)" /><el-button v-if="canApprove(detail.summary)" type="success" plain :loading="approvalLoadingId === String(detail.summary.orderId)" @click="openApproval(detail.summary)"><CircleCheck :size="15" />审核通过</el-button><el-button v-if="workflowEligible(detail.summary) && can('platform:firstleg:inbound:view')" type="primary" plain @click="openWorkflow(detail.summary)"><ClipboardList :size="15" />后半程作业</el-button></div></div>
|
||||
<dl class="detail-grid"><div><dt>客户</dt><dd>{{ customerName(detail.summary) }}</dd></div><div><dt>入仓码</dt><dd>{{ detail.summary.inboundCode || '—' }}</dd></div><div><dt>平台收货仓</dt><dd>{{ detail.summary.receivingWarehouseName || '—' }}</dd></div><div><dt>目的仓</dt><dd>{{ detail.summary.destinationWarehouseName || '—' }}</dd></div><div><dt>承运商</dt><dd>{{ detail.order.order?.carrierName || '待平台指派' }}</dd></div><div><dt>下一步</dt><dd>{{ canApprove(detail.summary) ? '选择承运商并审核' : nextActionBoundary(detail.summary) || '—' }}</dd></div><div><dt>最近更新</dt><dd>{{ formatDate(detail.summary.updatedAt) }}</dd></div></dl>
|
||||
</section>
|
||||
<section class="detail-section" aria-label="状态历史"><h3>状态历史</h3><el-empty v-if="detail.timeline.length === 0" description="暂无状态历史" :image-size="56" /><ol v-else class="history-list"><li v-for="(item, index) in detail.timeline" :key="`${item.source || 'event'}-${item.occurredAt || index}`"><StatusBadge :status="statusLabel(item.mainStatus)" /><div><strong>{{ item.action || item.eventType || '状态更新' }}</strong><p>{{ item.operatorName || '系统' }} · {{ formatDate(item.occurredAt) }}</p><p v-if="item.remark">{{ item.remark }}</p></div></li></ol></section>
|
||||
<section class="detail-section" aria-label="内部备注"><h3>内部备注</h3><el-empty v-if="detail.order.internalNotes.length === 0" description="暂无内部备注" :image-size="56" /><ul v-else class="notes-list"><li v-for="item in detail.order.internalNotes" :key="String(item.id)"><p>{{ item.content }}</p><span>操作人 {{ item.createdBy || '系统' }} · {{ formatDate(item.createdAt) }}</span></li></ul></section>
|
||||
</div>
|
||||
</AppDrawer>
|
||||
</template>
|
||||
|
||||
<AppDialog v-model="noteOpen" title="添加内部备注" confirm-text="保存备注" :confirm-loading="noteSaving" @confirm="saveNote">
|
||||
<el-input v-model="noteContent" type="textarea" :rows="5" maxlength="2000" show-word-limit placeholder="请输入仅平台内部可见的备注" aria-label="内部备注内容" />
|
||||
</AppDialog>
|
||||
|
||||
<AppDialog v-model="approvalOpen" title="选择承运商并审核" confirm-text="指派并审核通过" confirm-type="success" :loading="carrierLoading" :confirm-loading="approvalSaving" :confirm-disabled="Boolean(carrierLoadError) || carriers.length === 0" destroy-on-close @confirm="assignCarrierAndApprove" @cancel="closeApproval">
|
||||
<el-alert title="订单审核通过后将进入履约流程;承运商名称由后端公共主数据生成快照。" type="info" :closable="false" show-icon />
|
||||
@@ -543,6 +765,7 @@ onMounted(() => {
|
||||
.page-intro h1 { margin: 0; color: var(--cc-text); font-size: 22px; line-height: 1.35; }
|
||||
.page-intro p { margin: 5px 0 0; color: var(--cc-muted); font-size: 13px; line-height: 1.6; }
|
||||
.page-intro :deep(.el-button) { flex: 0 0 auto; }
|
||||
.orders-query-panel { overflow: hidden; border: 1px solid var(--cc-border); border-radius: 8px; background: #fff; }
|
||||
.status-tabs { display: flex; max-width: 100%; overflow-x: auto; margin-bottom: 0; border-bottom: 1px solid var(--cc-border); scrollbar-width: thin; }
|
||||
.status-tab { position: relative; flex: 0 0 auto; min-height: 44px; padding: 0 18px; border: 0; background: transparent; color: var(--cc-muted); font: inherit; font-size: 14px; cursor: pointer; }
|
||||
.status-tab::after { position: absolute; right: 12px; bottom: 0; left: 12px; height: 3px; background: transparent; content: ''; }
|
||||
@@ -550,7 +773,7 @@ onMounted(() => {
|
||||
.status-tab:focus-visible { outline: 2px solid var(--cc-primary); outline-offset: -2px; }
|
||||
.status-tab.is-active { color: var(--cc-primary); font-weight: 600; }
|
||||
.status-tab.is-active::after { background: var(--cc-primary); }
|
||||
.status-tabs + :deep(.query-toolbar) { border-top: 0; }
|
||||
.orders-query-panel :deep(.query-toolbar) { border: 0; }
|
||||
.orders-load-error { margin-top: 12px; }
|
||||
.table-card { margin-top: 16px; overflow: hidden; border: 1px solid var(--cc-border); border-radius: 8px; background: #fff; }
|
||||
.row-actions { display: flex; align-items: center; gap: 4px; white-space: nowrap; }
|
||||
@@ -561,24 +784,57 @@ onMounted(() => {
|
||||
.primary-cell { display: grid; min-width: 0; gap: 4px; }
|
||||
.primary-cell strong { overflow-wrap: anywhere; }
|
||||
.primary-cell span { color: var(--cc-muted); font-size: 12px; overflow-wrap: anywhere; }
|
||||
.detail-content { display: grid; gap: 22px; min-width: 0; }
|
||||
.detail-summary, .detail-section { padding: 16px; border: 1px solid var(--cc-border); border-radius: 8px; background: #fff; }
|
||||
.detail-summary__header { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; }
|
||||
.detail-summary__actions { display: flex; align-items: center; flex-wrap: wrap; justify-content: flex-end; gap: 8px; }
|
||||
.eyebrow { color: var(--cc-muted); font-size: 12px; }
|
||||
.detail-summary h2 { margin: 4px 0 0; color: var(--cc-text); font-size: 18px; }
|
||||
.detail-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px 20px; margin: 20px 0 0; }
|
||||
.detail-grid div { min-width: 0; }
|
||||
.detail-grid dt { color: var(--cc-muted); font-size: 12px; }
|
||||
.detail-grid dd { margin: 4px 0 0; color: var(--cc-text); overflow-wrap: anywhere; }
|
||||
.detail-section h3 { margin: 0 0 12px; color: var(--cc-text); font-size: 15px; }
|
||||
.history-list, .notes-list { display: grid; gap: 12px; margin: 0; padding: 0; list-style: none; }
|
||||
.history-list li { display: flex; align-items: flex-start; gap: 10px; padding-bottom: 12px; border-bottom: 1px solid #edf0f4; }
|
||||
.history-list li:last-child, .notes-list li:last-child { padding-bottom: 0; border-bottom: 0; }
|
||||
.history-list strong { font-size: 13px; }
|
||||
.history-list p, .notes-list p { margin: 3px 0 0; color: var(--cc-muted); font-size: 12px; line-height: 1.5; overflow-wrap: anywhere; }
|
||||
.notes-list li { padding-bottom: 12px; border-bottom: 1px solid #edf0f4; }
|
||||
.order-detail-header { display: flex; min-width: 0; align-items: center; justify-content: space-between; gap: 20px; margin-bottom: 18px; }
|
||||
.order-detail-heading { display: flex; min-width: 0; align-items: center; gap: 12px; }
|
||||
.back-button { flex: 0 0 auto; }
|
||||
.order-mark { display: grid; width: 44px; height: 44px; flex: 0 0 44px; place-items: center; border-radius: 8px; background: #1d2939; color: #fff; }
|
||||
.order-title { min-width: 0; }
|
||||
.order-title > span { display: block; margin-bottom: 2px; color: var(--cc-muted); font-size: 11px; }
|
||||
.order-title h1 { margin: 0; overflow: hidden; color: #172033; font-size: 22px; line-height: 1.25; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.order-title p { margin: 4px 0 0; color: var(--cc-muted); font-size: 12px; }
|
||||
.detail-header-actions { display: flex; flex: 0 0 auto; gap: 8px; }
|
||||
.detail-header-actions :deep(.el-button + .el-button) { margin-left: 0; }
|
||||
.detail-load-error { min-height: 280px; }
|
||||
.order-detail-content { min-height: 520px; }
|
||||
.detail-overview { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); overflow: hidden; margin-bottom: 16px; border: 1px solid var(--cc-border); border-radius: 8px; background: #fff; }
|
||||
.detail-overview article { min-width: 0; padding: 18px 20px; border-right: 1px solid var(--cc-border); }
|
||||
.detail-overview article:last-child { border-right: 0; }
|
||||
.detail-overview article > span, .detail-overview article > small { display: block; overflow: hidden; color: var(--cc-muted); font-size: 11px; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.detail-overview strong { display: block; margin: 7px 0 5px; overflow: hidden; color: #172033; font-size: 20px; line-height: 1.2; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.detail-overview strong small { margin-left: 5px; color: #475467; font-size: 11px; }
|
||||
.detail-tabs { display: grid; grid-template-columns: repeat(6, minmax(124px, 1fr)); overflow-x: auto; border: 1px solid var(--cc-border); border-bottom: 0; border-radius: 8px 8px 0 0; background: #fff; }
|
||||
.detail-tabs button { position: relative; display: flex; min-height: 56px; align-items: center; justify-content: center; gap: 7px; padding: 0 14px; border: 0; border-right: 1px solid #edf0f4; background: transparent; color: #667085; font-weight: 650; cursor: pointer; }
|
||||
.detail-tabs button:last-child { border-right: 0; }
|
||||
.detail-tabs button::after { position: absolute; right: 14px; bottom: 0; left: 14px; height: 3px; background: transparent; content: ''; }
|
||||
.detail-tabs button:hover { background: #fafbff; color: #344054; }
|
||||
.detail-tabs button.active { background: #f7f8ff; color: var(--cc-primary); }
|
||||
.detail-tabs button.active::after { background: var(--cc-primary); }
|
||||
.detail-panel { min-width: 0; min-height: 390px; padding: 22px; border: 1px solid var(--cc-border); border-radius: 0 0 8px 8px; background: #fff; }
|
||||
.panel-title { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; margin-bottom: 18px; padding-bottom: 14px; border-bottom: 1px solid var(--cc-border); }
|
||||
.panel-title h2 { margin: 0; color: #344054; font-size: 16px; }
|
||||
.panel-title p { margin: 5px 0 0; color: var(--cc-muted); font-size: 11px; }
|
||||
.panel-title > svg { color: var(--cc-primary); }
|
||||
.order-info-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 1px; margin: 0; overflow: hidden; border: 1px solid #edf0f4; background: #edf0f4; }
|
||||
.order-info-grid > div, .measurement-grid > div { min-width: 0; padding: 14px 16px; background: #fff; }
|
||||
.order-info-grid dt, .measurement-grid dt { margin-bottom: 6px; color: var(--cc-muted); font-size: 11px; }
|
||||
.order-info-grid dd, .measurement-grid dd { margin: 0; color: #25324a; font-size: 13px; font-weight: 650; line-height: 1.5; overflow-wrap: anywhere; }
|
||||
.detail-table-wrap { overflow-x: auto; }
|
||||
.detail-table-wrap :deep(.el-table) { min-width: 720px; --el-table-header-bg-color: #f8fafc; }
|
||||
.measurement-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 1px; margin: 0; border: 1px solid var(--cc-border); background: var(--cc-border); }
|
||||
.history-list, .notes-list { display: grid; gap: 0; margin: 0; padding: 0; list-style: none; }
|
||||
.history-list li { position: relative; display: grid; grid-template-columns: 16px minmax(0, 1fr); gap: 12px; padding-bottom: 20px; }
|
||||
.history-list li:not(:last-child)::after { position: absolute; top: 15px; bottom: 0; left: 7px; width: 1px; background: var(--cc-border); content: ''; }
|
||||
.history-marker { z-index: 1; width: 14px; height: 14px; margin-top: 3px; border: 3px solid var(--cc-primary); border-radius: 50%; background: #fff; }
|
||||
.history-line { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
|
||||
.history-list strong { color: #344054; font-size: 13px; }
|
||||
.history-list p, .notes-list p { margin: 4px 0 0; color: var(--cc-muted); font-size: 12px; line-height: 1.55; overflow-wrap: anywhere; }
|
||||
.history-list .history-remark { margin-top: 8px; padding: 10px 12px; background: #f8fafc; color: #475467; }
|
||||
.notes-list li { padding: 14px 0; border-bottom: 1px solid #edf0f4; }
|
||||
.notes-list li:first-child { padding-top: 0; }
|
||||
.notes-list li:last-child { padding-bottom: 0; border-bottom: 0; }
|
||||
.notes-list li > p { margin: 0; color: var(--cc-text); font-size: 13px; }
|
||||
.notes-list span { display: block; margin-top: 5px; color: var(--cc-muted); font-size: 12px; }
|
||||
@media (max-width: 720px) { .page-intro { align-items: stretch; flex-direction: column; } .page-intro :deep(.el-button) { width: 100%; min-height: 44px; } .status-tab { min-height: 46px; padding: 0 14px; } .detail-grid { grid-template-columns: 1fr; } .detail-summary__header { flex-direction: column; } .detail-summary__actions { justify-content: flex-start; } .row-actions { flex-wrap: wrap; } }
|
||||
@media (max-width: 960px) { .detail-overview { grid-template-columns: repeat(2, minmax(0, 1fr)); } .detail-overview article:nth-child(2) { border-right: 0; } .detail-overview article:nth-child(-n+2) { border-bottom: 1px solid var(--cc-border); } .order-info-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } .measurement-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
|
||||
@media (max-width: 720px) { .page-intro { align-items: stretch; flex-direction: column; } .page-intro :deep(.el-button) { width: 100%; min-height: 44px; } .status-tab { min-height: 46px; padding: 0 14px; } .table-actions :deep(.el-button) { width: 100%; min-height: 40px; } .row-actions { flex-wrap: wrap; } .order-detail-header { align-items: stretch; flex-direction: column; } .order-detail-heading { display: grid; grid-template-columns: auto 40px minmax(0, 1fr) auto; } .order-mark { width: 40px; height: 40px; } .detail-header-actions { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); } .detail-header-actions :deep(.el-button) { min-height: 44px; } .detail-overview, .order-info-grid, .measurement-grid { grid-template-columns: 1fr; } .detail-overview article { border-right: 0; border-bottom: 1px solid var(--cc-border); } .detail-overview article:last-child { border-bottom: 0; } .detail-tabs { grid-template-columns: repeat(6, 132px); } .detail-panel { padding: 16px; } }
|
||||
@media (max-width: 420px) { .order-detail-heading { grid-template-columns: 1fr auto; } .back-button { grid-column: 1 / -1; justify-self: start; } .order-mark { display: none; } .detail-header-actions { grid-template-columns: 1fr; } }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user