This commit is contained in:
2026-08-07 14:47:11 +08:00
parent e7cc0cf18b
commit 7ac173fac4
21 changed files with 1714 additions and 791 deletions
@@ -3,9 +3,9 @@ import { http } from '@/shared/api/http'
import {
approvePlatformUnifiedOrder,
addPlatformUnifiedOrderNote,
assignPlatformUnifiedOrderCarrier,
getPlatformUnifiedOrder,
listPlatformUnifiedOrders,
listPlatformUnifiedOrderFeeDetails,
} from './unifiedOrders'
vi.mock('@/shared/api/http', () => ({
@@ -52,11 +52,11 @@ describe('unified orders API contract', () => {
it('normalizes C02 row IDs and keeps inboundId for measurement routing', async () => {
vi.mocked(http.get).mockResolvedValue({ data: {
records: [{ orderId: '90001', tenantId: '208', inboundId: '77', inboundVersion: 3, mainStatus: 'WAIT_MEASURE', measurement: null, preload: null, loading: null, delivery: null, transport: null }],
records: [{ orderId: '90001', tenantId: '208', inboundId: '77', inboundVersion: 3, mainStatus: 'WAIT_MEASURE', customerRemark: '客户备注', latestInternalNote: '最新内部备注', measurement: null, preload: null, loading: null, delivery: null, transport: null }],
page: 1, size: 20, total: 1, pages: 1,
} })
await expect(listPlatformUnifiedOrders({ page: 1, size: 20 })).resolves.toMatchObject({
records: [{ orderId: '90001', tenantId: '208', inboundId: '77', inboundVersion: 3 }],
records: [{ orderId: '90001', tenantId: '208', inboundId: '77', inboundVersion: 3, customerRemark: '客户备注', latestInternalNote: '最新内部备注' }],
})
})
@@ -72,14 +72,11 @@ describe('unified orders API contract', () => {
expect(http.get).toHaveBeenCalledWith('/platform/v1/orders/90001', { params: { tenantId: '208' } })
})
it('uses the explicit first-leg carrier and approval boundaries for pending review', async () => {
vi.mocked(http.put).mockResolvedValue({ data: null })
it('uses the explicit first-leg approval boundary for pending review', async () => {
vi.mocked(http.post).mockResolvedValue({ data: null })
await assignPlatformUnifiedOrderCarrier('208', '90001', '30001')
await approvePlatformUnifiedOrder('208', '90001')
expect(http.put).toHaveBeenCalledWith('/platform/v1/orders/first-leg/90001/carrier', { carrierId: '30001' }, { params: { tenantId: '208' } })
expect(http.post).toHaveBeenCalledWith('/platform/v1/orders/first-leg/90001/approve', undefined, { params: { tenantId: '208' } })
})
@@ -88,4 +85,10 @@ describe('unified orders API contract', () => {
await expect(addPlatformUnifiedOrderNote('208', '90001', '已确认船期')).resolves.toBe('91001')
expect(http.post).toHaveBeenCalledWith('/platform/v1/orders/90001/internal-notes', { content: '已确认船期' }, { params: { tenantId: '208' } })
})
it('loads fee detail rows from the inbound fee-detail contract', async () => {
vi.mocked(http.get).mockResolvedValue({ data: [{ id: '1', inboundId: '88', reviewStatus: 'APPROVED', amount: 100 }] })
await expect(listPlatformUnifiedOrderFeeDetails('208', '88')).resolves.toHaveLength(1)
expect(http.get).toHaveBeenCalledWith('/platform/v1/inbounds/88/fee-details', { params: { accountId: '208' } })
})
})
@@ -124,6 +124,7 @@ export interface UnifiedOrderResource {
expressTrackingNo?: string | null
remark?: string | null
customerRemark?: string | null
latestInternalNote?: string | null
internalRemark?: string | null
createdBy?: string | null
}
@@ -208,6 +209,36 @@ export interface UnifiedOrderDetailResource {
timeline: UnifiedOrderTimelineEvent[]
}
export interface UnifiedOrderFeeDetailResource {
id: number | string
inboundId: number | string
inboundCode: string | null
reviewStatus: string | null
direction: string | null
customerName: string | null
invoiceTitle: string | null
businessDate: string | null
channelName: string | null
destinationName: string | null
fbaNo?: string | null
packageCount: number | null
actualGrossWeightKg: number | null
actualVolumeCbm: number | null
feeTypeNameSnapshot: string | null
currency: string | null
amount: number | null
chargeableQuantity: number | null
chargeUnit: string | null
unitPrice: number | null
inboundedAt: string | null
outboundAt?: string | null
storageDays?: number | null
settlementIncluded: boolean | null
billNo?: string | null
settlementStartDate?: string | null
settlementEndDate?: string | null
}
export interface UnifiedOrderPageResource {
records: UnifiedOrderResource[]
page: number
@@ -327,14 +358,6 @@ export async function getPlatformUnifiedOrder(tenantId: string, orderId: string)
return { ...result, summary: normalizeRow(result.summary) }
}
export function assignPlatformUnifiedOrderCarrier(tenantId: string, orderId: string, carrierId: string) {
return http.put<ApiEnvelope<void>, ApiEnvelope<void>>(
`/platform/v1/orders/first-leg/${orderId}/carrier`,
{ carrierId },
{ params: { tenantId } },
).then(unwrapApiResponse)
}
export function approvePlatformUnifiedOrder(tenantId: string, orderId: string) {
return http.post<ApiEnvelope<void>, ApiEnvelope<void>>(
`/platform/v1/orders/first-leg/${orderId}/approve`,
@@ -351,6 +374,14 @@ export function addPlatformUnifiedOrderNote(tenantId: string, orderId: string, c
).then(unwrapApiResponse)
}
export async function listPlatformUnifiedOrderFeeDetails(accountId: string, inboundId: string) {
const response = await http.get<ApiEnvelope<UnifiedOrderFeeDetailResource[]>, ApiEnvelope<UnifiedOrderFeeDetailResource[]>>(
`/platform/v1/inbounds/${encodeURIComponent(inboundId)}/fee-details`,
{ params: { accountId } },
)
return unwrapApiResponse(response)
}
export function unifiedOrderStatusLabel(status: UnifiedOrderMainStatus) {
return unifiedOrderStatusOptions.find((item) => item.value === status)?.label || status
}
@@ -59,6 +59,8 @@ describe('UnifiedOrdersView contract', () => {
expect(source).toContain('slotName: \'orderStatus\'')
expect(source).toContain('slotName: \'measurementReviewStatus\'')
expect(source).toContain("NOT_SUBMITTED: '未提交'")
expect(source).toContain("{ key: 'customerRemark', prop: 'customerRemark', label: '备注'")
expect(source).toContain("{ key: 'latestInternalNote', prop: 'latestInternalNote', label: '客服内部备注'")
})
it('keeps only confirmed business actions as explicit boundaries', () => {
@@ -72,7 +74,7 @@ describe('UnifiedOrdersView contract', () => {
expect(source).not.toContain('/actions/')
})
it('approves pending review orders without requiring a carrier assignment', () => {
it('approves pending review orders without exposing an order carrier assignment', () => {
expect(source).toContain("row.nextAction === 'APPROVE_ORDER'")
expect(source).toContain("can('platform:order:approve')")
expect(source).toContain('await approvePlatformUnifiedOrder(tenantId, orderId)')
@@ -80,8 +82,8 @@ describe('UnifiedOrdersView contract', () => {
expect(source).toContain('confirm-text="审核通过"')
expect(source).not.toContain('assignPlatformUnifiedOrderCarrier')
expect(source).not.toContain('listPlatformPartners')
expect(source).not.toContain('请选择启用的承运商')
expect(source).toContain("detail.order.order?.carrierName || '待平台指派'")
expect(source).not.toContain('指派承运商')
expect(source).not.toContain('待平台指派')
})
it('provides unified detail, history and internal note states', () => {
@@ -89,9 +91,22 @@ describe('UnifiedOrdersView contract', () => {
expect(source).toContain('aria-label="状态历史"')
expect(source).toContain('aria-label="内部备注"')
expect(source).toContain('订单详情暂时无法加载')
expect(source).toContain('一、客户下单数据')
expect(source).toContain('二、外箱规数据')
expect(source).toContain('三、实际测量数据')
expect(source).toContain('measurementCollapsed = reactive({ customer: false, outer: false, actual: false })')
expect(source).toContain('function toggleMeasurementSection(section: keyof typeof measurementCollapsed)')
expect(source).toContain(':aria-expanded="!measurementCollapsed.customer"')
expect(source).toContain(':aria-expanded="!measurementCollapsed.outer"')
expect(source).toContain(':aria-expanded="!measurementCollapsed.actual"')
expect(source).toContain('class="measurement-section-summary"')
expect(source).toContain('<ChevronUp v-if="!measurementCollapsed.customer"')
expect(source).toContain('<ChevronDown v-else :size="18" />')
expect(source).toContain("{ key: 'girthCm', label: '箱规周长(cm)' }")
expect(source).toContain('runMoreAction($event, row as UnifiedOrderResource)')
expect(source).toContain('aria-label="更多订单操作"')
expect(source).toContain("can('platform:order:note')")
expect(source).toContain('await loadOrders()')
})
it('renders order detail as a routed full page instead of a drawer', () => {
@@ -110,6 +125,8 @@ describe('UnifiedOrdersView contract', () => {
expect(source).toContain(`<dt>${label}</dt>`)
}
expect(source).toContain("partyValue(detail.order.parties, 'SENDER', 'phone')")
expect(source).toContain("partyValue(detail.order.parties, 'RECEIVER', 'phone')")
expect(source).not.toContain("partyValue(detail.order.parties, 'CONSIGNEE'")
expect(source).toContain("cargoTotal(detail.order.cargoes, 'netWeightKg')")
expect(source).toContain('暂无费用数据')
})
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed, nextTick, onMounted, ref, watch } from 'vue'
import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue'
import { ElMessage } from 'element-plus'
import { ArrowLeft, Box, ChevronDown, ChevronUp, CircleCheck, Eye, History, ListChecks, MoreHorizontal, PackageOpen, RefreshCw, Ruler, SlidersHorizontal, StickyNote, WalletCards } from '@lucide/vue'
import { useRoute, useRouter } from 'vue-router'
@@ -7,11 +7,13 @@ import {
getPlatformUnifiedOrder,
approvePlatformUnifiedOrder,
addPlatformUnifiedOrderNote,
listPlatformUnifiedOrderFeeDetails,
listPlatformUnifiedOrders,
unifiedOrderStatusOptions,
type UnifiedOrderDetailResource,
type UnifiedOrderMainStatus,
type UnifiedOrderResource,
type UnifiedOrderFeeDetailResource,
} from '@/modules/unified-orders/api/unifiedOrders'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam'
@@ -23,6 +25,11 @@ import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.v
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
import StatusBadge from '@/shared/components/StatusBadge.vue'
import {
downloadWeightImage,
getInboundMeasurement,
type InboundMeasurementDetail,
} from '@/modules/inbounds'
type FilterModel = QueryModel & {
tenantId: string
@@ -74,6 +81,15 @@ const noteOpen = ref(false)
const noteSaving = ref(false)
const noteContent = ref('')
const noteOrder = ref<UnifiedOrderResource | null>(null)
const feeDetails = ref<UnifiedOrderFeeDetailResource[]>([])
const feeLoading = ref(false)
const feeLoadedKey = ref('')
const measurementDetail = ref<InboundMeasurementDetail | null>(null)
const measurementLoading = ref(false)
const measurementLoadedKey = ref('')
const measurementImageUrls = ref<string[]>([])
const measurementCollapsed = reactive({ customer: false, outer: false, actual: false })
let detailLoadingKey = ''
const showAdvancedFilters = ref(false)
const filters = ref<FilterModel>(emptyFilters())
const orderId = computed(() => typeof route.params.orderId === 'string' ? route.params.orderId : '')
@@ -87,6 +103,16 @@ const detailTabs = [
{ key: 'other', label: '其他', icon: StickyNote },
]
const customerMeasurementColumns = [
{ key: 'itemNo', label: '#' }, { key: 'lengthCm', label: '箱规长(cm)' }, { key: 'widthCm', label: '箱规宽(cm)' },
{ key: 'heightCm', label: '箱规高(cm)' }, { key: 'packageCount', label: '箱数' }, { key: 'unitWeightKg', label: '每箱毛重' },
{ key: 'unitVolumeCbm', label: '每箱体积' }, { key: 'remark', label: '备注' },
]
const measuredColumns = [
...customerMeasurementColumns.slice(0, 7), { key: 'girthCm', label: '箱规周长(cm)' }, { key: 'totalWeightKg', label: '总毛重(T.K.GW)' },
{ key: 'totalVolumeCbm', label: '总体积(T.CBM)' }, { key: 'volumeWeightKg', label: '总材积' }, { key: 'remark', label: '备注' },
]
const statusTabs = computed(() => [
{ value: '' as const, label: '全部' },
...unifiedOrderStatusOptions,
@@ -200,8 +226,8 @@ const columns: DataTableColumn[] = [
{ 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: 'customerRemark', prop: 'customerRemark', label: '备注', minWidth: 180, slotName: 'field' },
{ key: 'latestInternalNote', prop: 'latestInternalNote', label: '客服内部备注', minWidth: 180, slotName: 'field' },
{ key: 'createdBy', prop: 'createdBy', label: '创建人', minWidth: 120, slotName: 'field' },
{ key: 'createdAt', prop: 'createdAt', label: '创建日期', minWidth: 150, slotName: 'field' },
]
@@ -259,13 +285,13 @@ function formatNumber(value: number | null | undefined, digits = 2) {
return value === null || value === undefined ? '—' : value.toLocaleString('zh-CN', { maximumFractionDigits: digits })
}
function partyValue(parties: Array<Record<string, unknown>>, type: 'SENDER' | 'CONSIGNEE', key: string) {
function partyValue(parties: Array<Record<string, unknown>>, type: 'SENDER' | 'RECEIVER', key: string) {
const party = parties.find((item) => String(item.partyType || '').toUpperCase() === type)
const value = party?.[key]
return value === null || value === undefined || value === '' ? '—' : String(value)
}
function partyAddress(parties: Array<Record<string, unknown>>, type: 'SENDER' | 'CONSIGNEE') {
function partyAddress(parties: Array<Record<string, unknown>>, type: 'SENDER' | 'RECEIVER') {
const party = parties.find((item) => String(item.partyType || '').toUpperCase() === type)
if (!party) return '—'
return ['province', 'city', 'district', 'addressLine'].map((key) => party[key]).filter((value) => typeof value === 'string' && value.trim()).join(' ') || '—'
@@ -336,6 +362,138 @@ function orderStatusLabel(value: string | null | undefined) {
} as Record<string, string>)[value || ''] || value || '—'
}
function cargoValue(cargo: Record<string, unknown>, key: string) {
const value = cargo[key]
if (key === 'circumferenceCm') {
const width = Number(cargo.widthCm)
const height = Number(cargo.heightCm)
return Number.isFinite(width) && Number.isFinite(height) ? formatNumber(2 * (width + height)) : '—'
}
if (value === null || value === undefined || value === '') return '—'
if (typeof value === 'number') return formatNumber(value)
return String(value)
}
function cargoImage(cargo: Record<string, unknown>) {
const value = cargo.imageUrl || cargo.cargoImageUrl || cargo.image
return typeof value === 'string' && value ? value : ''
}
function feeDate(value: string | null | undefined) {
return value ? formatDate(value) : '—'
}
function feeText(value: string | null | undefined) {
return value || '—'
}
function feeNumber(value: number | null | undefined) {
return value === null || value === undefined ? '—' : formatNumber(value, 4)
}
function feeReviewLabel(value: string | null | undefined) {
return ({ PENDING: '待审核', APPROVED: '审核通过', REJECTED: '审核驳回', NOT_SUBMITTED: '未提交' } as Record<string, string>)[value || ''] || value || '—'
}
function feeDirectionLabel(value: string | null | undefined) {
return ({ RECEIVABLE: '应收', PAYABLE: '应付' } as Record<string, string>)[value || ''] || value || '—'
}
async function loadFeeDetails() {
const summary = detail.value?.summary
if (!summary?.inboundId || !can('firstleg:inbound:fee-detail:view')) return
const key = `${summary.tenantId}:${summary.inboundId}`
if (feeLoadedKey.value === key || feeLoading.value) return
feeLoading.value = true
try {
feeDetails.value = await listPlatformUnifiedOrderFeeDetails(String(summary.tenantId), String(summary.inboundId))
feeLoadedKey.value = key
} catch {
feeDetails.value = []
} finally {
feeLoading.value = false
}
}
function measurementValue(row: Record<string, unknown>, key: string, customerSource = false) {
if (customerSource && key === 'unitWeightKg') {
const total = Number(row.grossWeightKg)
const count = Number(row.packageCount)
return Number.isFinite(total) && Number.isFinite(count) && count > 0 ? formatNumber(total / count, 4) : '—'
}
if (customerSource && key === 'unitVolumeCbm') {
const total = Number(row.volumeCbm)
const count = Number(row.packageCount)
return Number.isFinite(total) && Number.isFinite(count) && count > 0 ? formatNumber(total / count, 6) : '—'
}
if (customerSource && key === 'remark') return String(row.cargoRemark || '—')
const value = row[key]
if (value === null || value === undefined || value === '') return '—'
return typeof value === 'number' ? formatNumber(value, 6) : String(value)
}
interface MeasurementTotals {
count: number
gross: number
volume: number
volumetric: number
}
function measurementTotals(rows: Record<string, unknown>[], customerSource = false) {
const values = rows.reduce<MeasurementTotals>((result, row) => {
const count = Number(row.packageCount)
const gross = Number(customerSource ? row.grossWeightKg : row.totalWeightKg)
const volume = Number(customerSource ? row.volumeCbm : row.totalVolumeCbm)
const volumetric = Number(row.volumeWeightKg)
if (Number.isFinite(count)) result.count += count
if (Number.isFinite(gross)) result.gross += gross
if (Number.isFinite(volume)) result.volume += volume
if (Number.isFinite(volumetric)) result.volumetric += volumetric
return result
}, { count: 0, gross: 0, volume: 0, volumetric: 0 })
return {
count: values.count || null, gross: values.gross || null, volume: values.volume || null,
volumetric: values.volumetric || null, weightVolume: values.volume || null,
}
}
function revokeMeasurementImages() {
measurementImageUrls.value.forEach((url) => URL.revokeObjectURL(url))
measurementImageUrls.value = []
}
async function loadMeasurementDetails() {
const summary = detail.value?.summary
if (!summary?.inboundId || !can('platform:firstleg:inbound:view')) return
const key = `${summary.tenantId}:${summary.inboundId}`
if (measurementLoadedKey.value === key || measurementLoading.value) return
measurementLoading.value = true
try {
measurementDetail.value = await getInboundMeasurement(String(summary.tenantId), String(summary.inboundId))
measurementLoadedKey.value = key
revokeMeasurementImages()
const fileIds = [...measurementDetail.value.items.map((item) => item.intelligentImageFileId), ...measurementDetail.value.weightResults.map((item) => item.fileId)]
.filter((value): value is string => Boolean(value))
const blobs = await Promise.all(fileIds.map((fileId) => downloadWeightImage(String(summary.tenantId), String(summary.inboundId), fileId).catch(() => null)))
measurementImageUrls.value = blobs.filter((blob): blob is Blob => Boolean(blob)).map((blob) => URL.createObjectURL(blob))
} catch {
measurementDetail.value = null
revokeMeasurementImages()
} finally {
measurementLoading.value = false
}
}
function selectDetailTab(key: string) {
activeDetailTab.value = key
if (key === 'fees') void loadFeeDetails()
if (key === 'measurement') void loadMeasurementDetails()
}
function toggleMeasurementSection(section: keyof typeof measurementCollapsed) {
measurementCollapsed[section] = !measurementCollapsed[section]
}
function nextActionBoundary(row: UnifiedOrderResource) {
return row.nextAction === 'CONFIRM_MEASUREMENT' ? '待完成测量' : ''
}
@@ -411,8 +569,8 @@ async function saveNote() {
const target = noteOrder.value
await addPlatformUnifiedOrderNote(String(target.tenantId), String(target.orderId), content)
noteOpen.value = false
if (detail.value?.summary.orderId === target.orderId) await loadDetail()
else await loadOrders()
await loadOrders()
if (detail.value?.summary.orderId === target.orderId) await loadDetail(true)
noteOrder.value = null
ElMessage.success('内部备注已保存')
} catch (error) {
@@ -545,13 +703,21 @@ function closeDetail() {
void router.push({ name: 'unified-orders' })
}
async function loadDetail() {
async function loadDetail(force = false) {
if (!orderId.value) return
const tenantId = typeof route.query.tenantId === 'string' ? route.query.tenantId : ''
if (!tenantId) {
detailError.value = { message: '订单详情缺少客户标识,请从订单列表重新打开' }
return
}
const requestKey = `${tenantId}:${orderId.value}`
if (!force && (detailLoadingKey === requestKey || detail.value?.summary.orderId === orderId.value && String(detail.value.summary.tenantId) === tenantId)) return
detailLoadingKey = requestKey
feeDetails.value = []
feeLoadedKey.value = ''
measurementDetail.value = null
measurementLoadedKey.value = ''
revokeMeasurementImages()
detailLoading.value = true
detailError.value = null
try {
@@ -561,6 +727,7 @@ async function loadDetail() {
detailError.value = getApiErrorDetails(error, '统一订单详情加载失败,请检查 C02 统一详情接口和订单权限')
} finally {
detailLoading.value = false
if (detailLoadingKey === requestKey) detailLoadingKey = ''
}
}
@@ -574,7 +741,7 @@ watch(orderId, (value) => {
onMounted(() => {
void loadCustomers()
void loadWarehouses()
void loadOrders()
if (!orderId.value) void loadOrders()
})
</script>
@@ -613,7 +780,7 @@ onMounted(() => {
</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>
<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="selectDetailTab(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">
@@ -633,11 +800,9 @@ onMounted(() => {
<div><dt>目的仓联系人</dt><dd>{{ detail.summary.destinationContact || '—' }}</dd></div>
<div><dt>目的仓联系人电话</dt><dd>{{ detail.summary.destinationContactPhone || '—' }}</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>{{ detail.order.order?.tradeTerm || '—' }}</dd></div>
<div><dt>贸易方式</dt><dd>{{ detail.order.order?.tradeTerm || detail.summary.businessType || '—' }}</dd></div>
<div><dt>业务类型</dt><dd>{{ detail.summary.businessType || '—' }}</dd></div>
<div><dt>贸易条款</dt><dd>{{ detail.order.order?.tradeTerm || '—' }}</dd></div>
<div><dt>买单 / 退税</dt><dd>{{ detail.order.order?.taxMode || '—' }}</dd></div>
<div><dt>报关方式</dt><dd>{{ detail.summary.businessType || '—' }}</dd></div>
<div><dt>费用审核状态</dt><dd>{{ detail.summary.feeReviewStatus || '—' }}</dd></div>
<div><dt>是否扣货</dt><dd>{{ displayBoolean(detail.summary.holdCargo) }}</dd></div>
<div><dt>扣货理由</dt><dd>{{ detail.summary.holdReason || '—' }}</dd></div>
@@ -671,9 +836,9 @@ onMounted(() => {
<div><dt>测量完成时间</dt><dd>{{ formatDate(detail.summary.measurementCompletedAt || detail.summary.measurement?.completedAt) }}</dd></div>
<div class="wide"><dt>提货联系人</dt><dd>{{ partyValue(detail.order.parties, 'SENDER', 'contactName') }} · {{ detail.summary.pickupContactPhone || partyValue(detail.order.parties, 'SENDER', 'phone') }} · {{ detail.summary.pickupContactEmail || partyValue(detail.order.parties, 'SENDER', 'email') }}</dd></div>
<div class="wide"><dt>提货地址</dt><dd>{{ detail.summary.pickupAddress || partyAddress(detail.order.parties, 'SENDER') }} · {{ detail.summary.pickupPostalCode || partyValue(detail.order.parties, 'SENDER', 'postalCode') }}</dd></div>
<div class="wide"><dt>目的地联系人</dt><dd>{{ partyValue(detail.order.parties, 'CONSIGNEE', 'contactName') }} · {{ partyValue(detail.order.parties, 'CONSIGNEE', 'phone') }} · {{ partyValue(detail.order.parties, 'CONSIGNEE', 'email') }}</dd></div>
<div><dt>目的仓联系人邮箱</dt><dd>{{ partyValue(detail.order.parties, 'CONSIGNEE', 'email') }}</dd></div>
<div class="wide"><dt>目的地地址</dt><dd>{{ partyAddress(detail.order.parties, 'CONSIGNEE') }} · {{ partyValue(detail.order.parties, 'CONSIGNEE', 'postalCode') }}</dd></div>
<div class="wide"><dt>目的地联系人</dt><dd>{{ partyValue(detail.order.parties, 'RECEIVER', 'contactName') }} · {{ partyValue(detail.order.parties, 'RECEIVER', 'phone') }} · {{ partyValue(detail.order.parties, 'RECEIVER', 'email') }}</dd></div>
<div><dt>目的仓联系人邮箱</dt><dd>{{ partyValue(detail.order.parties, 'RECEIVER', 'email') }}</dd></div>
<div class="wide"><dt>目的地地址</dt><dd>{{ partyAddress(detail.order.parties, 'RECEIVER') }} · {{ partyValue(detail.order.parties, 'RECEIVER', 'postalCode') }}</dd></div>
<div class="wide"><dt>备注</dt><dd>{{ detail.order.order?.customerRemark || '—' }}</dd></div>
<div class="wide"><dt>关联文件</dt><dd>{{ detail.summary.associatedFiles?.join('、') || '—' }}</dd></div>
<div class="wide"><dt>唛头文件</dt><dd>{{ detail.summary.markFiles?.join('、') || '—' }}</dd></div>
@@ -690,13 +855,56 @@ onMounted(() => {
<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-column prop="itemNo" label="#" width="64" />
<el-table-column label="报关HS_CODE" min-width="130"><template #default="{ row }">{{ cargoValue(row as Record<string, unknown>, 'hsCode') }}</template></el-table-column>
<el-table-column label="货品图片" width="110"><template #default="{ row }"><el-image v-if="cargoImage(row as Record<string, unknown>)" :src="cargoImage(row as Record<string, unknown>)" fit="contain" class="cargo-image" /><span v-else>—</span></template></el-table-column>
<el-table-column label="SKU NO." min-width="150"><template #default="{ row }">{{ cargoValue(row as Record<string, unknown>, 'skuNo') }}</template></el-table-column>
<el-table-column label="中文品名" min-width="170"><template #default="{ row }">{{ cargoValue(row as Record<string, unknown>, 'cargoName') }}</template></el-table-column>
<el-table-column label="英文品名" min-width="170"><template #default="{ row }">{{ cargoValue(row as Record<string, unknown>, 'cargoNameEn') }}</template></el-table-column>
<el-table-column label="清关HS_CODE" min-width="130"><template #default="{ row }">{{ cargoValue(row as Record<string, unknown>, 'customsHsCode') }}</template></el-table-column>
<el-table-column label="箱规长(cm)" min-width="110"><template #default="{ row }">{{ cargoValue(row as Record<string, unknown>, 'lengthCm') }}</template></el-table-column>
<el-table-column label="箱规宽(cm)" min-width="110"><template #default="{ row }">{{ cargoValue(row as Record<string, unknown>, 'widthCm') }}</template></el-table-column>
<el-table-column label="箱规高(cm)" min-width="110"><template #default="{ row }">{{ cargoValue(row as Record<string, unknown>, 'heightCm') }}</template></el-table-column>
<el-table-column label="箱规周长(cm)" min-width="120"><template #default="{ row }">{{ cargoValue(row as Record<string, unknown>, 'circumferenceCm') }}</template></el-table-column>
<el-table-column label="每箱产品数量" min-width="130"><template #default="{ row }">{{ cargoValue(row as Record<string, unknown>, 'unitsPerPackage') }}</template></el-table-column>
<el-table-column label="箱数" width="90"><template #default="{ row }">{{ cargoValue(row as Record<string, unknown>, 'packageCount') }}</template></el-table-column>
<el-table-column label="总毛重(T.KGW)" min-width="140"><template #default="{ row }">{{ cargoValue(row as Record<string, unknown>, 'grossWeightKg') }}</template></el-table-column>
<el-table-column label="总净重(T.KNW)" min-width="140"><template #default="{ row }">{{ cargoValue(row as Record<string, unknown>, 'netWeightKg') }}</template></el-table-column>
</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" />
<div class="panel-title"><div><h2>费用信息</h2><p>共 {{ feeDetails.length }} 条应收应付费用明细</p></div><WalletCards :size="19" /></div>
<el-alert v-if="!can('firstleg:inbound:fee-detail:view')" title="当前账号没有查看费用明细的权限" type="warning" :closable="false" show-icon />
<div v-else v-loading="feeLoading" class="detail-table-wrap">
<el-table :data="feeDetails" empty-text="暂无费用数据" row-key="id">
<el-table-column label="审核状态" min-width="120"><template #default="{ row }"><StatusBadge :status="feeReviewLabel((row as UnifiedOrderFeeDetailResource).reviewStatus)" /></template></el-table-column>
<el-table-column label="应收/应付" min-width="110"><template #default="{ row }">{{ feeDirectionLabel((row as UnifiedOrderFeeDetailResource).direction) }}</template></el-table-column>
<el-table-column label="客户公司名" min-width="170"><template #default="{ row }">{{ feeText((row as UnifiedOrderFeeDetailResource).customerName) }}</template></el-table-column>
<el-table-column label="裹箱头" min-width="150"><template #default="{ row }">{{ feeText((row as UnifiedOrderFeeDetailResource).invoiceTitle) }}</template></el-table-column>
<el-table-column label="入仓码" min-width="150"><template #default="{ row }">{{ feeText((row as UnifiedOrderFeeDetailResource).inboundCode) }}</template></el-table-column>
<el-table-column label="业务发生日" min-width="130"><template #default="{ row }">{{ feeDate((row as UnifiedOrderFeeDetailResource).businessDate) }}</template></el-table-column>
<el-table-column label="渠道" min-width="130"><template #default="{ row }">{{ feeText((row as UnifiedOrderFeeDetailResource).channelName) }}</template></el-table-column>
<el-table-column label="目的仓" min-width="150"><template #default="{ row }">{{ feeText((row as UnifiedOrderFeeDetailResource).destinationName) }}</template></el-table-column>
<el-table-column label="FBA编号" min-width="130"><template #default="{ row }">{{ feeText((row as UnifiedOrderFeeDetailResource).fbaNo) }}</template></el-table-column>
<el-table-column label="箱数" width="90"><template #default="{ row }">{{ feeNumber((row as UnifiedOrderFeeDetailResource).packageCount) }}</template></el-table-column>
<el-table-column label="毛重" min-width="110"><template #default="{ row }">{{ feeNumber((row as UnifiedOrderFeeDetailResource).actualGrossWeightKg) }}</template></el-table-column>
<el-table-column label="体积" min-width="110"><template #default="{ row }">{{ feeNumber((row as UnifiedOrderFeeDetailResource).actualVolumeCbm) }}</template></el-table-column>
<el-table-column label="费用类型" min-width="150"><template #default="{ row }">{{ feeText((row as UnifiedOrderFeeDetailResource).feeTypeNameSnapshot) }}</template></el-table-column>
<el-table-column label="货币" width="90"><template #default="{ row }">{{ feeText((row as UnifiedOrderFeeDetailResource).currency) }}</template></el-table-column>
<el-table-column label="应收金额" min-width="120"><template #default="{ row }">{{ feeNumber((row as UnifiedOrderFeeDetailResource).amount) }}</template></el-table-column>
<el-table-column label="计费体称/重量" min-width="150"><template #default="{ row }">{{ feeNumber((row as UnifiedOrderFeeDetailResource).chargeableQuantity) }} {{ (row as UnifiedOrderFeeDetailResource).chargeUnit || '' }}</template></el-table-column>
<el-table-column label="单价" min-width="110"><template #default="{ row }">{{ feeNumber((row as UnifiedOrderFeeDetailResource).unitPrice) }}</template></el-table-column>
<el-table-column label="入仓时间" min-width="150"><template #default="{ row }">{{ feeDate((row as UnifiedOrderFeeDetailResource).inboundedAt) }}</template></el-table-column>
<el-table-column label="出仓时间" min-width="150"><template #default="{ row }">{{ feeDate((row as UnifiedOrderFeeDetailResource).outboundAt) }}</template></el-table-column>
<el-table-column label="仓储天数" min-width="110"><template #default="{ row }">{{ feeNumber((row as UnifiedOrderFeeDetailResource).storageDays) }}</template></el-table-column>
<el-table-column label="参与结算" min-width="110"><template #default="{ row }">{{ (row as UnifiedOrderFeeDetailResource).settlementIncluded == null ? '—' : (row as UnifiedOrderFeeDetailResource).settlementIncluded ? '是' : '否' }}</template></el-table-column>
<el-table-column label="账单编号" min-width="140"><template #default="{ row }">{{ feeText((row as UnifiedOrderFeeDetailResource).billNo) }}</template></el-table-column>
<el-table-column label="结算起始日" min-width="130"><template #default="{ row }">{{ feeDate((row as UnifiedOrderFeeDetailResource).settlementStartDate) }}</template></el-table-column>
<el-table-column label="结算结束日" min-width="130"><template #default="{ row }">{{ feeDate((row as UnifiedOrderFeeDetailResource).settlementEndDate) }}</template></el-table-column>
</el-table>
</div>
</template>
<template v-else-if="activeDetailTab === 'history'">
@@ -707,7 +915,28 @@ onMounted(() => {
<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>
<section class="measurement-section">
<button type="button" class="measurement-section-title" :aria-expanded="!measurementCollapsed.customer" @click="toggleMeasurementSection('customer')"><h3>一、客户下单数据</h3><span class="measurement-section-summary"><span>总件数:{{ measurementTotals(detail.order.cargoes, true).count ?? '—' }}</span><span>总毛重:{{ formatNumber(measurementTotals(detail.order.cargoes, true).gross) }}</span><span>总体积:{{ formatNumber(measurementTotals(detail.order.cargoes, true).volume, 6) }}</span><span>总材积重:{{ formatNumber(measurementTotals(detail.order.cargoes, true).volumetric) }}</span><span>重量体积:{{ formatNumber(measurementTotals(detail.order.cargoes, true).weightVolume, 6) }}</span></span><ChevronUp v-if="!measurementCollapsed.customer" :size="18" /><ChevronDown v-else :size="18" /></button>
<div v-if="!measurementCollapsed.customer">
<div class="detail-table-wrap"><el-table :data="detail.order.cargoes" empty-text="暂无客户下单数据"><el-table-column v-for="column in customerMeasurementColumns" :key="column.key" :label="column.label" :prop="column.key" min-width="120"><template #default="{ row }">{{ measurementValue(row as Record<string, unknown>, column.key, true) }}</template></el-table-column></el-table></div>
<div class="measurement-total"><strong>合计</strong><span>箱数:{{ measurementTotals(detail.order.cargoes, true).count ?? '—' }}</span><span>总毛重:{{ formatNumber(measurementTotals(detail.order.cargoes, true).gross) }}</span><span>总体积:{{ formatNumber(measurementTotals(detail.order.cargoes, true).volume, 6) }}</span><span>总材积:{{ formatNumber(measurementTotals(detail.order.cargoes, true).volumetric) }}</span></div>
</div>
</section>
<section class="measurement-section">
<button type="button" class="measurement-section-title" :aria-expanded="!measurementCollapsed.outer" @click="toggleMeasurementSection('outer')"><h3>二、外箱规数据</h3><span class="measurement-section-summary"><span>总件数:{{ measurementTotals((measurementDetail?.items || []) as unknown as Record<string, unknown>[]).count ?? '—' }}</span><span>总毛重:{{ formatNumber(measurementTotals((measurementDetail?.items || []) as unknown as Record<string, unknown>[]).gross) }}</span><span>总体积:{{ formatNumber(measurementTotals((measurementDetail?.items || []) as unknown as Record<string, unknown>[]).volume, 6) }}</span><span>总材积重:{{ formatNumber(measurementTotals((measurementDetail?.items || []) as unknown as Record<string, unknown>[]).volumetric) }}</span><span>重量体积:{{ formatNumber(measurementTotals((measurementDetail?.items || []) as unknown as Record<string, unknown>[]).weightVolume, 6) }}</span></span><ChevronUp v-if="!measurementCollapsed.outer" :size="18" /><ChevronDown v-else :size="18" /></button>
<div v-if="!measurementCollapsed.outer">
<div v-loading="measurementLoading" class="detail-table-wrap"><el-table :data="measurementDetail?.items || []" empty-text="暂无外箱规数据"><el-table-column v-for="column in measuredColumns" :key="column.key" :label="column.label" :prop="column.key" min-width="120"><template #default="{ row }">{{ measurementValue(row as Record<string, unknown>, column.key) }}</template></el-table-column></el-table></div>
<div class="measurement-total"><strong>合计</strong><span>箱数:{{ measurementTotals((measurementDetail?.items || []) as unknown as Record<string, unknown>[]).count ?? '—' }}</span><span>总毛重:{{ formatNumber(measurementTotals((measurementDetail?.items || []) as unknown as Record<string, unknown>[]).gross) }}</span><span>总体积:{{ formatNumber(measurementTotals((measurementDetail?.items || []) as unknown as Record<string, unknown>[]).volume, 6) }}</span><span>总材积:{{ formatNumber(measurementTotals((measurementDetail?.items || []) as unknown as Record<string, unknown>[]).volumetric) }}</span></div>
</div>
</section>
<section class="measurement-section">
<button type="button" class="measurement-section-title" :aria-expanded="!measurementCollapsed.actual" @click="toggleMeasurementSection('actual')"><h3>三、实际测量数据</h3><span class="measurement-section-summary"><span>总件数:{{ measurementDetail?.actualPackageCount ?? '—' }}</span><span>总毛重:{{ formatNumber(measurementDetail?.actualGrossWeightKg) }}</span><span>总体积:{{ formatNumber(measurementDetail?.actualVolumeCbm, 6) }}</span><span>总材积重:{{ formatNumber(measurementDetail?.volumeWeightKg) }}</span><span>重量体积:{{ formatNumber(measurementDetail?.actualVolumeCbm, 6) }}</span></span><ChevronUp v-if="!measurementCollapsed.actual" :size="18" /><ChevronDown v-else :size="18" /></button>
<div v-if="!measurementCollapsed.actual">
<div v-loading="measurementLoading" class="detail-table-wrap"><el-table :data="measurementDetail?.items || []" empty-text="暂无实际测量数据"><el-table-column v-for="column in measuredColumns" :key="column.key" :label="column.label" :prop="column.key" min-width="120"><template #default="{ row }">{{ measurementValue(row as Record<string, unknown>, column.key) }}</template></el-table-column></el-table></div>
<div class="measurement-total"><strong>合计</strong><span>箱数:{{ measurementDetail?.actualPackageCount ?? '—' }}</span><span>总毛重:{{ formatNumber(measurementDetail?.actualGrossWeightKg) }}</span><span>总体积:{{ formatNumber(measurementDetail?.actualVolumeCbm, 6) }}</span><span>总材积:{{ formatNumber(measurementDetail?.volumeWeightKg) }}</span></div>
<div class="measurement-media"><h4>测量图片</h4><div v-if="measurementImageUrls.length" class="measurement-media-grid"><el-image v-for="url in measurementImageUrls" :key="url" :src="url" fit="contain" :preview-src-list="measurementImageUrls" class="measurement-media-image" /></div><el-empty v-else description="暂无测量图片" :image-size="48" /><h4>测量视频</h4><el-empty description="暂无测量视频" :image-size="48" /></div>
</div>
</section>
</template>
<template v-else>
@@ -797,6 +1026,7 @@ onMounted(() => {
<AppDialog v-model="approvalOpen" title="审核订单" confirm-text="审核通过" confirm-type="success" :confirm-loading="approvalSaving" destroy-on-close @confirm="approveOrder" @cancel="closeApproval">
<el-alert title="确认订单资料无误后审核通过,订单将进入待测量阶段。" type="info" :closable="false" show-icon />
</AppDialog>
</section>
</template>
@@ -860,7 +1090,22 @@ onMounted(() => {
.order-info-grid dd, .measurement-grid dd { min-width: 0; margin: 0; color: #25324a; font-size: 12px; font-weight: 650; line-height: 1.35; overflow-wrap: anywhere; }
.detail-table-wrap { overflow-x: auto; }
.detail-table-wrap :deep(.el-table) { min-width: 720px; --el-table-header-bg-color: #f8fafc; }
.cargo-image { width: 72px; height: 56px; border: 1px solid var(--cc-border); border-radius: 6px; background: #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); }
.measurement-section { margin-top: 18px; }
.measurement-section-title { display: flex; width: 100%; min-height: 48px; align-items: center; gap: 16px; padding: 9px 14px; border: 1px solid var(--cc-border); border-radius: 7px 7px 0 0; background: #f8fafc; color: var(--cc-text); font: inherit; text-align: left; cursor: pointer; }
.measurement-section-title:hover { background: #f2f6fb; }
.measurement-section-title:focus-visible { outline: 2px solid var(--cc-primary); outline-offset: 2px; }
.measurement-section-title h3 { flex: 0 0 auto; margin: 0; color: var(--cc-text); font-size: 14px; }
.measurement-section-summary { display: flex; min-width: 0; flex: 1; align-items: center; justify-content: flex-end; gap: 18px; color: var(--cc-muted); font-size: 12px; white-space: nowrap; }
.measurement-section-title > svg { flex: 0 0 auto; color: var(--cc-muted); }
.measurement-total { display: flex; min-height: 42px; align-items: center; justify-content: flex-end; gap: 24px; padding: 9px 14px; border: 1px solid var(--cc-border); background: #f8fafc; color: var(--cc-muted); font-size: 12px; }
.measurement-total { border-top: 0; border-radius: 0 0 7px 7px; }
.measurement-total strong { margin-right: auto; color: var(--cc-text); font-size: 13px; }
.measurement-media { margin-top: 20px; }
.measurement-media h4 { margin: 18px 0 10px; color: var(--cc-text); font-size: 14px; }
.measurement-media-grid { display: flex; flex-wrap: wrap; gap: 12px; }
.measurement-media-image { width: 110px; height: 90px; border: 1px solid var(--cc-border); border-radius: 6px; background: #f8fafc; }
.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: ''; }
@@ -875,6 +1120,6 @@ onMounted(() => {
.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: 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 36px minmax(0, 1fr) auto; } .detail-header-actions { display: grid; grid-template-columns: 1fr; } .detail-header-actions :deep(.el-button) { min-height: 40px; } .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; } .order-info-grid > div.wide { grid-column: auto; } .detail-tabs { grid-template-columns: repeat(6, 132px); } .detail-panel { padding: 16px; } }
@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 36px minmax(0, 1fr) auto; } .detail-header-actions { display: grid; grid-template-columns: 1fr; } .detail-header-actions :deep(.el-button) { min-height: 40px; } .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; } .order-info-grid > div.wide { grid-column: auto; } .detail-tabs { grid-template-columns: repeat(6, 132px); } .detail-panel { padding: 16px; } .measurement-section-title { align-items: flex-start; flex-wrap: wrap; padding: 12px; } .measurement-section-summary { width: 100%; flex-basis: calc(100% - 34px); flex-wrap: wrap; justify-content: flex-start; gap: 6px 16px; white-space: normal; } .measurement-section-title > svg { margin-left: auto; } .measurement-total { flex-wrap: wrap; justify-content: flex-start; gap: 6px 16px; } .measurement-total strong { width: 100%; } }
@media (max-width: 420px) { .order-detail-heading { grid-template-columns: auto minmax(0, 1fr) auto; } .order-mark { display: none; } }
</style>