This commit is contained in:
2026-08-27 11:38:59 +08:00
parent 82f0479928
commit 550d8add97
5 changed files with 132 additions and 12 deletions
@@ -112,10 +112,10 @@ describe('platform container loading API', () => {
container: { id: 9007199254740993, warehouseId: 9007199254740994, version: '4' }, loadingPlanId: 9007199254740995, container: { id: 9007199254740993, warehouseId: 9007199254740994, version: '4' }, loadingPlanId: 9007199254740995,
pda: { taskId: 9007199254740996, loadingPlanId: 9007199254740995, containerBookingId: 9007199254740993, taskVersion: '2', itemCount: '4', completedItemCount: '3', unresolvedItemCount: '1', photoCounts: [{ evidenceType: 'EMPTY_CONTAINER', count: '2' }], photos: [{ evidenceId: 20, tenantId: 21, inboundId: 22, fileId: 23, downloadPath: '/api/platform/v1/containers/9007199254740993/pda/photos/23/download' }], exceptionReasons: [] }, pda: { taskId: 9007199254740996, loadingPlanId: 9007199254740995, containerBookingId: 9007199254740993, taskVersion: '2', itemCount: '4', completedItemCount: '3', unresolvedItemCount: '1', photoCounts: [{ evidenceType: 'EMPTY_CONTAINER', count: '2' }], photos: [{ evidenceId: 20, tenantId: 21, inboundId: 22, fileId: 23, downloadPath: '/api/platform/v1/containers/9007199254740993/pda/photos/23/download' }], exceptionReasons: [] },
documents: [{ id: 9007199254740997, documentType: 'DELIVERY_ORDER', fileId: 9007199254740998, uploadedBy: 9, version: '2' }], documents: [{ id: 9007199254740997, documentType: 'DELIVERY_ORDER', fileId: 9007199254740998, uploadedBy: 9, version: '2' }],
exception: { active: false, markedBy: null }, exceptionHistory: [{ id: 10, operatorId: 11 }], timeline: [{ operatorId: 12 }], exception: { active: false, markedBy: null }, exceptionHistory: [{ id: 10, operatorId: 11 }], timeline: [{ id: '2092438843630397001', eventId: '2092438843630397002', idempotencyKey: '2092438843630397003', operatorId: 12, eventType: 'PUSH_PRELOAD', detail: '推送预装清单' }],
} }) } })
const result = await getContainerOverview('7') const result = await getContainerOverview('7')
expect(result).toMatchObject({ container: { id: expect.any(String), warehouseId: expect.any(String), version: 4 }, loadingPlanId: expect.any(String), pda: { taskId: expect.any(String), itemCount: 4, photoCounts: [{ count: 2 }] }, documents: [{ fileId: expect.any(String), version: 2 }] }) expect(result).toMatchObject({ container: { id: expect.any(String), warehouseId: expect.any(String), version: 4 }, loadingPlanId: expect.any(String), pda: { taskId: expect.any(String), itemCount: 4, photoCounts: [{ count: 2 }] }, documents: [{ fileId: expect.any(String), version: 2 }], timeline: [{ id: '2092438843630397001', eventId: '2092438843630397002', idempotencyKey: '2092438843630397003', operatorId: '12' }] })
expect(http.get).toHaveBeenCalledWith('/platform/v1/containers/7/overview') expect(http.get).toHaveBeenCalledWith('/platform/v1/containers/7/overview')
vi.mocked(http.post).mockResolvedValueOnce({ code: '200', data: null }) vi.mocked(http.post).mockResolvedValueOnce({ code: '200', data: null })
@@ -60,7 +60,13 @@ function overview(raw: ContainerOverview): ContainerOverview {
documents: (raw.documents || []).map(overviewDocument), documents: (raw.documents || []).map(overviewDocument),
exception: { ...raw.exception, markedBy: optionalId(raw.exception?.markedBy) }, exception: { ...raw.exception, markedBy: optionalId(raw.exception?.markedBy) },
exceptionHistory: (raw.exceptionHistory || []).map((entry) => ({ ...entry, id: id(entry.id), operatorId: optionalId(entry.operatorId) })), exceptionHistory: (raw.exceptionHistory || []).map((entry) => ({ ...entry, id: id(entry.id), operatorId: optionalId(entry.operatorId) })),
timeline: (raw.timeline || []).map((entry) => ({ ...entry, operatorId: optionalId(entry.operatorId) })), timeline: (raw.timeline || []).map((entry) => ({
...entry,
id: optionalId(entry.id),
eventId: optionalId(entry.eventId),
idempotencyKey: optionalId(entry.idempotencyKey),
operatorId: optionalId(entry.operatorId),
})),
} }
} }
+12
View File
@@ -149,8 +149,20 @@ export interface ContainerExceptionHistory {
} }
export interface ContainerTimelineEntry { export interface ContainerTimelineEntry {
id?: string | null
eventId?: string | null
idempotencyKey?: string | null
eventType: string eventType: string
action?: string | null
fromStatus?: string | null
toStatus?: string | null
detail: string | null detail: string | null
summary?: string | null
changeSummary?: string | null
reason?: string | null
changedFields?: unknown
fieldChanges?: unknown
changes?: unknown
operatorId: string | null operatorId: string | null
operatorName: string | null operatorName: string | null
occurredAt: string | null occurredAt: string | null
@@ -63,6 +63,40 @@ describe('CarrierScheduleQueryPanel loading states', () => {
expect(getPortSchedule).not.toHaveBeenCalled() expect(getPortSchedule).not.toHaveBeenCalled()
}) })
it('filters large no-op status batches, keeps real actions, deduplicates only adjacent explicit event keys and sorts newest first', async () => {
vi.mocked(listCarrierScheduleQueries).mockResolvedValue({ records: [], page: 1, size: 200, total: 0, pages: 0 })
const noops = Array.from({ length: 40 }, (_, index) => ({
eventType: 'STATUS_UPDATE', action: 'STATUS_UPDATE', fromStatus: 'EDITING', toStatus: 'EDITING',
detail: index % 2 ? null : '预装柜中→预装柜中', eventId: `2092438843630398${String(index).padStart(3, '0')}`,
operatorName: '系统', occurredAt: `2026-08-27T09:${String(index).padStart(2, '0')}:00`,
}))
const timeline = [
...noops,
{ eventType: 'STATUS_UPDATE', action: 'STATUS_UPDATE', fromStatus: 'EDITING', toStatus: 'LOADING', detail: null, eventId: '2092438843630399001', operatorName: '调度员', occurredAt: '2026-08-27T10:00:00' },
{ eventType: 'STATUS_UPDATE', action: 'STATUS_UPDATE', fromStatus: 'LOADING', toStatus: 'LOADING', detail: null, changedFields: ['containerNo'], eventId: '2092438843630399002', operatorName: '调度员', occurredAt: '2026-08-27T10:01:00' },
{ eventType: 'STATUS_UPDATE', action: 'PUSH_PRELOAD', fromStatus: 'EDITING', toStatus: 'EDITING', detail: null, eventId: '2092438843630399003', operatorName: '调度员', occurredAt: '2026-08-27T10:02:00' },
{ eventType: 'PDA_BATCH', action: 'PDA_BATCH', fromStatus: 'LOADING', toStatus: 'LOADING', detail: '提交拣货结果', eventId: '2092438843630399004', operatorName: '仓库员', occurredAt: '2026-08-27T10:03:00' },
{ eventType: 'PDA_BATCH', action: 'PDA_BATCH', fromStatus: 'LOADING', toStatus: 'LOADING', detail: '提交拣货结果', eventId: '2092438843630399004', operatorName: '仓库员', occurredAt: '2026-08-27T10:04:00' },
{ eventType: 'STATUS_UPDATE', action: 'CONFIRM_PRELOAD', fromStatus: 'EDITING', toStatus: 'EDITING', detail: null, idempotencyKey: '2092438843630399005', operatorName: '复核员', occurredAt: '2026-08-27T10:05:00' },
{ eventType: 'TRANSPORT_UPDATE', action: 'TRANSPORT_UPDATE', detail: '更新集装箱号', eventId: '2092438843630399006', operatorName: '操作员甲', occurredAt: '2026-08-27T10:06:00' },
{ eventType: 'TRANSPORT_UPDATE', action: 'TRANSPORT_UPDATE', detail: '更新集装箱号', eventId: '2092438843630399007', operatorName: '操作员乙', occurredAt: '2026-08-27T10:07:00' },
]
const wrapper = mount(CarrierScheduleQueryPanel, { props: { oceanBookingId: '2', warehouseTimeline: timeline }, global: { plugins: [ElementPlus, pinia] } })
await flushPromises()
expect(wrapper.find('.history-count').text()).toBe('仓内 7 条 · 共 7 条')
const rows = wrapper.findAll('.fulfillment-timeline li')
expect(rows).toHaveLength(7)
expect(rows[0]?.text()).toContain('操作员乙')
expect(rows[6]?.text()).toContain('预装柜中 → 拣货中')
expect(rows.filter((row) => row.text().includes('提交拣货结果'))).toHaveLength(1)
expect(wrapper.text()).toContain('推送预装清单')
expect(wrapper.text()).toContain('确认预装清单')
expect(wrapper.text()).toContain('业务字段已更新')
expect(wrapper.text()).not.toContain('2092438843630398000')
expect(rows.filter((row) => row.text().includes('更新集装箱号'))).toHaveLength(2)
})
it('falls back to the booking/container task when an automatic task has no linked port schedule', async () => { it('falls back to the booking/container task when an automatic task has no linked port schedule', async () => {
vi.mocked(listCarrierScheduleQueries).mockResolvedValue({ records: [{ ...queryView, query: { ...queryView.query, resultPortScheduleId: null } }], page: 1, size: 200, total: 1, pages: 1 }) vi.mocked(listCarrierScheduleQueries).mockResolvedValue({ records: [{ ...queryView, query: { ...queryView.query, resultPortScheduleId: null } }], page: 1, size: 200, total: 1, pages: 1 })
vi.mocked(getCarrierScheduleQuery).mockResolvedValue({ query: { ...queryView, query: { ...queryView.query, resultPortScheduleId: null } }, nodes: [], exceptions: [], histories: [] }) vi.mocked(getCarrierScheduleQuery).mockResolvedValue({ query: { ...queryView, query: { ...queryView.query, resultPortScheduleId: null } }, nodes: [], exceptions: [], histories: [] })
@@ -10,7 +10,13 @@ import { appendCarrierScheduleManualNode, getCarrierScheduleQuery, listCarrierSc
import { getPortSchedule } from '../api/platformPortSchedules' import { getPortSchedule } from '../api/platformPortSchedules'
import type { CarrierScheduleExceptionFact, CarrierScheduleManualNodeRequest, CarrierScheduleNodeFact, CarrierScheduleQueryDetail, CarrierScheduleQueryView, CarrierScheduleStandardNode, PortScheduleView } from '../types' import type { CarrierScheduleExceptionFact, CarrierScheduleManualNodeRequest, CarrierScheduleNodeFact, CarrierScheduleQueryDetail, CarrierScheduleQueryView, CarrierScheduleStandardNode, PortScheduleView } from '../types'
type WarehouseTimelineEntry = { eventType: string; detail?: string | null; operatorName?: string | null; occurredAt?: string | null } type WarehouseTimelineEntry = {
id?: string | null; eventId?: string | null; idempotencyKey?: string | null
eventType: string; action?: string | null; fromStatus?: string | null; toStatus?: string | null
detail?: string | null; summary?: string | null; changeSummary?: string | null; reason?: string | null
changedFields?: unknown; fieldChanges?: unknown; changes?: unknown
operatorName?: string | null; occurredAt?: string | null
}
type FulfillmentEvent = { key: string; time: string; type: '仓内操作' | '确认离仓' | '船运节点' | '人工补录' | '异常'; title: string; summary: string; source: string; actual: boolean } type FulfillmentEvent = { key: string; time: string; type: '仓内操作' | '确认离仓' | '船运节点' | '人工补录' | '异常'; title: string; summary: string; source: string; actual: boolean }
const props = defineProps<{ oceanBookingId?: string | null; containerBookingId?: string | null; portScheduleId?: string | null; warehouseTimeline?: WarehouseTimelineEntry[] }>() const props = defineProps<{ oceanBookingId?: string | null; containerBookingId?: string | null; portScheduleId?: string | null; warehouseTimeline?: WarehouseTimelineEntry[] }>()
const access = useAccessPreviewStore() const access = useAccessPreviewStore()
@@ -86,17 +92,78 @@ function exceptionLocation(item: CarrierScheduleExceptionFact) { return [item.lo
function nodeTimeText(item: CarrierScheduleNodeFact) { return item.actualAt ? `实际 ${formatTime(item.actualAt)}` : item.estimatedAt ? `预计 ${formatTime(item.estimatedAt)}` : `观测 ${formatTime(item.observedAt)}` } function nodeTimeText(item: CarrierScheduleNodeFact) { return item.actualAt ? `实际 ${formatTime(item.actualAt)}` : item.estimatedAt ? `预计 ${formatTime(item.estimatedAt)}` : `观测 ${formatTime(item.observedAt)}` }
function exceptionText(value: CarrierScheduleExceptionFact['exceptionCode']) { return ({ CUSTOMS_HOLD: '海关扣留', TERMINAL_HOLD: '码头扣留', DOCUMENT_HOLD: '单证扣留', UNKNOWN_HOLD: '其他扣留' } as Record<string, string>)[value] || value } function exceptionText(value: CarrierScheduleExceptionFact['exceptionCode']) { return ({ CUSTOMS_HOLD: '海关扣留', TERMINAL_HOLD: '码头扣留', DOCUMENT_HOLD: '单证扣留', UNKNOWN_HOLD: '其他扣留' } as Record<string, string>)[value] || value }
function historyText(action: string) { return ({ AUTO_CREATE: '国内出仓自动建任务', CREATE: '人工建立查询任务', NODE_OBSERVED: '记录标准节点', PROVIDER_QUERY: '船司查询', MANUAL_NODE: '人工补录标准节点', STOP: '停止查询' } as Record<string, string>)[action] || action } function historyText(action: string) { return ({ AUTO_CREATE: '国内出仓自动建任务', CREATE: '人工建立查询任务', NODE_OBSERVED: '记录标准节点', PROVIDER_QUERY: '船司查询', MANUAL_NODE: '人工补录标准节点', STOP: '停止查询' } as Record<string, string>)[action] || action }
function warehouseEventText(value: string) { return ({ TRANSPORT_UPDATE: '更新运输信息', PDA_CLAIM: '领取 PDA 任务', PDA_BATCH: '提交拣货结果', PDA_COMPLETE_LOADING: '完成装柜' } as Record<string, string>)[value] || value || '状态更新' } function warehouseEventText(value: string) { return ({ STATUS_UPDATE: '状态更新', PUSH_PRELOAD: '推送预装清单', CONFIRM_PRELOAD: '确认预装清单', TRANSPORT_UPDATE: '更新运输信息', PDA_CLAIM: '领取 PDA 任务', PDA_BATCH: '提交拣货结果', PDA_COMPLETE_LOADING: '完成装柜' } as Record<string, string>)[value] || value || '状态更新' }
function nonBlank(value: unknown) { return typeof value === 'string' ? value.trim() : '' }
function hasStructuredChanges(value: unknown) {
if (Array.isArray(value)) return value.length > 0
if (value && typeof value === 'object') return Object.keys(value as Record<string, unknown>).length > 0
return Boolean(nonBlank(value))
}
function statusLabel(value?: string | null) {
return ({ WAIT_LOADING: '待装柜', EDITING: '预装柜中', LOADING: '拣货中', COMPLETED: '装柜完成' } as Record<string, string>)[value || ''] || nonBlank(value)
}
function isStatusEcho(detail: string, fromStatus?: string | null, toStatus?: string | null) {
const from = nonBlank(fromStatus); const to = nonBlank(toStatus)
if (!from || !to) return false
const compact = detail.replace(/\s+/g, '')
return [[from, to], [statusLabel(from), statusLabel(to)]].some(([left, right]) =>
[`${left}→${right}`, `${left}->${right}`].includes(compact))
}
function warehouseAction(entry: WarehouseTimelineEntry) { return nonBlank(entry.action) || nonBlank(entry.eventType) }
function hasEffectiveWarehouseChange(entry: WarehouseTimelineEntry) {
if (hasStructuredChanges(entry.changedFields) || hasStructuredChanges(entry.fieldChanges) || hasStructuredChanges(entry.changes)) return true
if ([entry.summary, entry.changeSummary, entry.reason].some((value) => nonBlank(value))) return true
const detail = nonBlank(entry.detail)
return Boolean(detail && !isStatusEcho(detail, entry.fromStatus, entry.toStatus))
}
function isMeaningfulWarehouseAction(entry: WarehouseTimelineEntry) {
return [entry.action, entry.eventType].some((value) => /^(?:PUSH_PRELOAD|PDA(?:_|$)|CONFIRM(?:_|$))/.test(nonBlank(value)))
}
function isNoopWarehouseStatus(entry: WarehouseTimelineEntry) {
if (isMeaningfulWarehouseAction(entry)) return false
return warehouseAction(entry) === 'STATUS_UPDATE'
&& nonBlank(entry.fromStatus) !== ''
&& nonBlank(entry.fromStatus) === nonBlank(entry.toStatus)
&& !hasEffectiveWarehouseChange(entry)
}
function eventIdentity(entry: WarehouseTimelineEntry) { return nonBlank(entry.eventId) || nonBlank(entry.idempotencyKey) }
function eventSignature(entry: WarehouseTimelineEntry) {
return JSON.stringify([warehouseAction(entry), entry.fromStatus || null, entry.toStatus || null, entry.summary || null, entry.changeSummary || null, entry.reason || null, entry.detail || null, entry.changedFields || null, entry.fieldChanges || null, entry.changes || null])
}
function sameExplicitEvent(left: WarehouseTimelineEntry, right: WarehouseTimelineEntry) {
const sameId = nonBlank(left.eventId) && nonBlank(left.eventId) === nonBlank(right.eventId)
const sameKey = nonBlank(left.idempotencyKey) && nonBlank(left.idempotencyKey) === nonBlank(right.idempotencyKey)
return Boolean((sameId || sameKey) && eventSignature(left) === eventSignature(right))
}
const filteredWarehouseTimeline = computed(() => {
const sorted = (props.warehouseTimeline || []).map((entry, index) => ({ entry, index }))
.filter(({ entry }) => entry.occurredAt && !['MARK', 'CANCEL'].includes(warehouseAction(entry)) && !isNoopWarehouseStatus(entry))
.sort((left, right) => {
const time = Date.parse(right.entry.occurredAt || '') - Date.parse(left.entry.occurredAt || '')
return time || left.index - right.index
})
const result: WarehouseTimelineEntry[] = []
for (const { entry } of sorted) {
const previous = result[result.length - 1]
if (previous && sameExplicitEvent(previous, entry)) continue
result.push(entry)
}
return result
})
function warehouseEventSummary(entry: WarehouseTimelineEntry) { function warehouseEventSummary(entry: WarehouseTimelineEntry) {
const value = String(entry.detail || '').trim() const value = nonBlank(entry.summary) || nonBlank(entry.changeSummary) || nonBlank(entry.reason) || nonBlank(entry.detail)
return !value || value.length > 120 || /(?:operation|idempot|claim-|pda-submit|complete-)/i.test(value) ? '操作已记录' : value if (value && value.length <= 120 && !/(?:operation|idempot|claim-|pda-submit|complete-)/i.test(value)) return value
const from = nonBlank(entry.fromStatus); const to = nonBlank(entry.toStatus)
if (from && to && from !== to) return `${statusLabel(from)} → ${statusLabel(to)}`
if (hasStructuredChanges(entry.changedFields) || hasStructuredChanges(entry.fieldChanges) || hasStructuredChanges(entry.changes)) return '业务字段已更新'
return '操作已记录'
} }
const fulfillmentEvents = computed<FulfillmentEvent[]>(() => { const fulfillmentEvents = computed<FulfillmentEvent[]>(() => {
const values: FulfillmentEvent[] = [] const values: FulfillmentEvent[] = []
for (const [index, item] of (props.warehouseTimeline || []).entries()) { for (const [index, item] of filteredWarehouseTimeline.value.entries()) {
if (!item.occurredAt || ['MARK', 'CANCEL'].includes(item.eventType)) continue const action = warehouseAction(item)
const departure = /OUTBOUND|DISPATCH|DEPARTED_WAREHOUSE/.test(item.eventType) const departure = /OUTBOUND|DISPATCH|DEPARTED_WAREHOUSE/.test(action)
values.push({ key: `warehouse-${item.eventType}-${item.occurredAt}-${index}`, time: item.occurredAt, type: departure ? '确认离仓' : '仓内操作', title: warehouseEventText(item.eventType), summary: warehouseEventSummary(item), source: item.operatorName || '系统', actual: true }) values.push({ key: `warehouse-${eventIdentity(item) || `${action}-${item.occurredAt}-${index}`}`, time: item.occurredAt!, type: departure ? '确认离仓' : '仓内操作', title: warehouseEventText(action), summary: warehouseEventSummary(item), source: item.operatorName || '系统', actual: true })
} }
const nodeKeys = new Set<string>() const nodeKeys = new Set<string>()
for (const item of primaryNodeFacts.value) { for (const item of primaryNodeFacts.value) {
@@ -179,7 +246,7 @@ watch(() => manualForm.actualAt, (actualAt) => {
<template> <template>
<section class="carrier-query" aria-label="货柜履约轨迹"> <section class="carrier-query" aria-label="货柜履约轨迹">
<header><div><h2>货柜履约轨迹</h2><p>统一展示仓内操作、确认离仓与船运节点;预计节点不会推进实际进度。</p></div><div class="header-actions"><el-button v-if="query && access.can('platform:firstleg:port-schedule-query:manual')" size="small" type="primary" plain @click="openManualDialog"><Plus :size="15" />人工补录节点</el-button><el-tag v-if="query" :type="statusType" effect="light">{{ statusText }}</el-tag></div></header> <header><div><h2>货柜履约轨迹</h2><p>统一展示仓内操作、确认离仓与船运节点;预计节点不会推进实际进度。</p></div><div class="header-actions"><span class="history-count">仓内 {{ filteredWarehouseTimeline.length }} 条 · 共 {{ fulfillmentEvents.length }} 条</span><el-button v-if="query && access.can('platform:firstleg:port-schedule-query:manual')" size="small" type="primary" plain @click="openManualDialog"><Plus :size="15" />人工补录节点</el-button><el-tag v-if="query" :type="statusType" effect="light">{{ statusText }}</el-tag></div></header>
<LoadErrorState v-if="error" compact title="船节点查询状态加载失败" :message="error.message" :loading="loading" @retry="load"><template #details><ApiErrorMeta :details="error" /></template></LoadErrorState> <LoadErrorState v-if="error" compact title="船节点查询状态加载失败" :message="error.message" :loading="loading" @retry="load"><template #details><ApiErrorMeta :details="error" /></template></LoadErrorState>
<div v-else-if="loading" class="loading-state"><el-skeleton :rows="1" animated /></div> <div v-else-if="loading" class="loading-state"><el-skeleton :rows="1" animated /></div>
<template v-else> <template v-else>
@@ -209,5 +276,6 @@ watch(() => manualForm.actualAt, (actualAt) => {
</template> </template>
<style scoped> <style scoped>
.history-count{color:var(--cc-muted);font-size:12px;white-space:nowrap}
.carrier-query{display:grid;gap:9px;padding-top:2px}.carrier-query>header{display:flex;align-items:flex-start;justify-content:space-between;gap:12px}.header-actions{display:flex;align-items:center;gap:8px}.carrier-query h2{margin:0;font-size:15px}.carrier-query header p{margin:3px 0 0;color:var(--cc-muted);font-size:12px}.loading-state{padding:6px 0}.latest-result{display:flex;flex-wrap:wrap;align-items:center;gap:6px 16px;padding:7px 10px;border-left:3px solid var(--el-color-primary);background:var(--el-color-primary-light-9);font-size:12px}.fulfillment-timeline{display:grid;margin:0;padding:0;list-style:none}.fulfillment-timeline li{display:grid;grid-template-columns:130px 86px 150px minmax(180px,1fr) minmax(120px,.8fr);align-items:center;gap:7px;padding:6px 2px;border-top:1px solid var(--cc-border);font-size:12px}.fulfillment-timeline time,.fulfillment-timeline span,.fulfillment-timeline small{overflow:hidden;color:var(--cc-muted);text-overflow:ellipsis;white-space:nowrap}.tracking-prompt{margin:0;padding:7px 10px;border-left:3px solid var(--el-color-info);background:var(--cc-bg-subtle,#f7f8fa);color:var(--cc-muted);font-size:12px}.empty-line{margin:0;color:var(--cc-muted);font-size:12px}.technical-details{border-top:1px solid var(--cc-border);font-size:12px}.technical-details summary{padding:7px 2px;color:var(--cc-primary);cursor:pointer}.query-facts{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:8px 16px;margin:4px 0 10px}.query-facts>div{min-width:0}.query-facts dt{color:var(--cc-muted);font-size:11px}.query-facts dd{margin:2px 0 0;font-size:12px}.break-value{overflow-wrap:anywhere}.technical-details h3{margin:8px 0 5px;font-size:12px}.technical-history{margin:0;padding:0;list-style:none}.technical-history li{display:grid;grid-template-columns:130px 150px minmax(0,1fr);gap:8px;padding:5px 0;border-top:1px solid var(--cc-border)}.technical-history small{color:var(--cc-muted)}.manual-form{display:grid;gap:2px;margin-top:12px}.manual-form-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}@media(max-width:1200px){.query-facts{grid-template-columns:repeat(2,minmax(0,1fr))}.fulfillment-timeline li{grid-template-columns:120px 82px 140px minmax(0,1fr)}.fulfillment-timeline small{grid-column:4}}@media(max-width:720px){.carrier-query>header{align-items:flex-start;flex-direction:column}.query-facts,.manual-form-grid{grid-template-columns:1fr}.fulfillment-timeline li{grid-template-columns:90px minmax(0,1fr)}.fulfillment-timeline strong,.fulfillment-timeline span,.fulfillment-timeline small{grid-column:2}.technical-history li{grid-template-columns:1fr}} .carrier-query{display:grid;gap:9px;padding-top:2px}.carrier-query>header{display:flex;align-items:flex-start;justify-content:space-between;gap:12px}.header-actions{display:flex;align-items:center;gap:8px}.carrier-query h2{margin:0;font-size:15px}.carrier-query header p{margin:3px 0 0;color:var(--cc-muted);font-size:12px}.loading-state{padding:6px 0}.latest-result{display:flex;flex-wrap:wrap;align-items:center;gap:6px 16px;padding:7px 10px;border-left:3px solid var(--el-color-primary);background:var(--el-color-primary-light-9);font-size:12px}.fulfillment-timeline{display:grid;margin:0;padding:0;list-style:none}.fulfillment-timeline li{display:grid;grid-template-columns:130px 86px 150px minmax(180px,1fr) minmax(120px,.8fr);align-items:center;gap:7px;padding:6px 2px;border-top:1px solid var(--cc-border);font-size:12px}.fulfillment-timeline time,.fulfillment-timeline span,.fulfillment-timeline small{overflow:hidden;color:var(--cc-muted);text-overflow:ellipsis;white-space:nowrap}.tracking-prompt{margin:0;padding:7px 10px;border-left:3px solid var(--el-color-info);background:var(--cc-bg-subtle,#f7f8fa);color:var(--cc-muted);font-size:12px}.empty-line{margin:0;color:var(--cc-muted);font-size:12px}.technical-details{border-top:1px solid var(--cc-border);font-size:12px}.technical-details summary{padding:7px 2px;color:var(--cc-primary);cursor:pointer}.query-facts{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:8px 16px;margin:4px 0 10px}.query-facts>div{min-width:0}.query-facts dt{color:var(--cc-muted);font-size:11px}.query-facts dd{margin:2px 0 0;font-size:12px}.break-value{overflow-wrap:anywhere}.technical-details h3{margin:8px 0 5px;font-size:12px}.technical-history{margin:0;padding:0;list-style:none}.technical-history li{display:grid;grid-template-columns:130px 150px minmax(0,1fr);gap:8px;padding:5px 0;border-top:1px solid var(--cc-border)}.technical-history small{color:var(--cc-muted)}.manual-form{display:grid;gap:2px;margin-top:12px}.manual-form-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}@media(max-width:1200px){.query-facts{grid-template-columns:repeat(2,minmax(0,1fr))}.fulfillment-timeline li{grid-template-columns:120px 82px 140px minmax(0,1fr)}.fulfillment-timeline small{grid-column:4}}@media(max-width:720px){.carrier-query>header{align-items:flex-start;flex-direction:column}.query-facts,.manual-form-grid{grid-template-columns:1fr}.fulfillment-timeline li{grid-template-columns:90px minmax(0,1fr)}.fulfillment-timeline strong,.fulfillment-timeline span,.fulfillment-timeline small{grid-column:2}.technical-history li{grid-template-columns:1fr}}
</style> </style>