fix(admin): compact container departure gates

This commit is contained in:
2026-08-26 12:36:01 +08:00
parent e8c41bca2d
commit 4088e36f74
2 changed files with 133 additions and 17 deletions
@@ -29,7 +29,7 @@ const overview = {
const outboundDetail = { const outboundDetail = {
aggregate: { aggregate: {
containerBookingId: '2092438843630391297', cabinetNo: 'CAB-1', containerNo: 'C11111111', containerBookingId: '2092438843630391297', cabinetNo: 'CAB-1', containerNo: 'C11111111',
loadingPlanId: '2092438843630391401', loadingPlanStatus: 'COMPLETED', dispatchStatus: 'READY_FOR_DISPATCH', version: 4, loadingPlanId: '2092438843630391401', loadingPlanStatus: 'COMPLETED', truckAppointmentStatus: 'ARRIVED', dispatchStatus: 'READY_FOR_DISPATCH', version: 4,
inbounds: [{ orderId: '11', orderNo: 'O-1', inboundId: '2092438843630391501', inboundCode: 'IN-1', tenantId: '22', inboundStatus: 'LOADED' }], inbounds: [{ orderId: '11', orderNo: 'O-1', inboundId: '2092438843630391501', inboundCode: 'IN-1', tenantId: '22', inboundStatus: 'LOADED' }],
gate: { passed: true, checkedAt: '2026-08-26T12:00:00', inbounds: [{ inboundId: '2092438843630391501', inboundCode: 'IN-1', gate: { passed: true, checkedAt: '2026-08-26T12:00:00', items: [{ code: 'CUSTOMS_CLEARANCE', name: '报关协同完成', passed: true, actual: 'COMPLETED', requirement: 'COMPLETED' }] } }] }, gate: { passed: true, checkedAt: '2026-08-26T12:00:00', inbounds: [{ inboundId: '2092438843630391501', inboundCode: 'IN-1', gate: { passed: true, checkedAt: '2026-08-26T12:00:00', items: [{ code: 'CUSTOMS_CLEARANCE', name: '报关协同完成', passed: true, actual: 'COMPLETED', requirement: 'COMPLETED' }] } }] },
}, },
@@ -100,8 +100,14 @@ describe('ContainerDetailView', () => {
expect(getContainerOutboundDispatch).toHaveBeenCalledOnce() expect(getContainerOutboundDispatch).toHaveBeenCalledOnce()
expect(getContainerOutboundDispatch).toHaveBeenCalledWith('2092438843630391297') expect(getContainerOutboundDispatch).toHaveBeenCalledWith('2092438843630391297')
expect(wrapper.text()).toContain('国内出仓门禁') expect(wrapper.text()).toContain('国内出仓门禁')
expect(wrapper.text()).toContain('报关协同完成') expect(wrapper.find('.gate-summary').text()).toContain('总门禁 1')
expect(wrapper.text()).toContain('IN-1') expect(wrapper.find('.gate-summary').text()).toContain('已通过 1')
expect(wrapper.find('.gate-summary').text()).toContain('阻断 0')
expect(wrapper.find('.gate-list').text()).not.toContain('报关协同完成')
await wrapper.find('.gate-summary button').trigger('click')
expect(wrapper.find('.gate-list').text()).toContain('报关协同完成')
expect(wrapper.find('.gate-list').text()).toContain('覆盖 1 个入库码')
expect(wrapper.find('.gate-list').text()).toContain('IN-1')
expect(wrapper.text()).toContain('确认离仓') expect(wrapper.text()).toContain('确认离仓')
wrapper.unmount() wrapper.unmount()
}) })
@@ -142,14 +148,98 @@ describe('ContainerDetailView', () => {
...outboundDetail, ...outboundDetail,
aggregate: { aggregate: {
...outboundDetail.aggregate, ...outboundDetail.aggregate,
gate: { passed: false, checkedAt: '2026-08-26T12:00:00', inbounds: [{ inboundId: '2092438843630391501', inboundCode: 'IN-1', gate: { passed: false, checkedAt: '2026-08-26T12:00:00', items: [{ code: 'TRUCK_DEPARTED', name: '关联拖车已离仓', passed: false, actual: 'ARRIVED', requirement: 'DEPARTED或COMPLETED' }] } }] }, gate: { passed: false, checkedAt: '2026-08-26T12:00:00', inbounds: [{ inboundId: '2092438843630391501', inboundCode: 'IN-1', gate: { passed: false, checkedAt: '2026-08-26T12:00:00', items: [{ code: 'SAILING_SCHEDULE', name: '船期有效且未过期', passed: false, actual: 'EXPIRED', requirement: 'VALID' }] } }] },
}, },
} as never) } as never)
const wrapper = mountView(); await flushPromises() const wrapper = mountView(); await flushPromises()
expect(wrapper.text()).toContain('关联拖车已离仓') expect(wrapper.text()).toContain('船期有效且未过期')
expect(wrapper.text()).toContain('ARRIVED') expect(wrapper.text()).toContain('EXPIRED')
expect(wrapper.text()).toContain('DEPARTED或COMPLETED') expect(wrapper.text()).toContain('VALID')
expect(wrapper.find('.outbound-panel__actions button').exists()).toBe(false) expect(wrapper.find('.outbound-panel__actions button').exists()).toBe(false)
wrapper.unmount() wrapper.unmount()
}) })
it('deduplicates identical gates across inbounds and shows compact coverage counts', async () => {
const blocked = { code: 'CUSTOMS_CLEARANCE', name: '报关协同完成', passed: false, actual: 'MISSING', requirement: 'COMPLETED' }
const passed = { code: 'SAILING_SCHEDULE', name: '船期有效且未过期', passed: true, actual: 'VALID', requirement: 'VALID' }
vi.mocked(getContainerOutboundDispatch).mockResolvedValueOnce({
...outboundDetail,
aggregate: {
...outboundDetail.aggregate,
gate: {
passed: false,
checkedAt: '2026-08-26T12:00:00',
inbounds: [
{ inboundId: '101', inboundCode: 'IN-101', gate: { passed: false, checkedAt: '2026-08-26T12:00:00', items: [blocked, passed] } },
{ inboundId: '102', inboundCode: 'IN-102', gate: { passed: false, checkedAt: '2026-08-26T12:00:00', items: [blocked, passed] } },
],
},
},
} as never)
const wrapper = mountView(); await flushPromises()
expect(wrapper.find('.gate-summary').text()).toContain('总门禁 2')
expect(wrapper.find('.gate-summary').text()).toContain('已通过 1')
expect(wrapper.find('.gate-summary').text()).toContain('阻断 1')
expect(wrapper.findAll('.gate-row')).toHaveLength(1)
expect(wrapper.find('.gate-row').text()).toContain('影响 2 个入库码')
expect(wrapper.find('.gate-row').text()).toContain('IN-101')
expect(wrapper.find('.gate-row').text()).toContain('IN-102')
await wrapper.find('.gate-summary button').trigger('click')
expect(wrapper.findAll('.gate-row')).toHaveLength(2)
expect(wrapper.findAll('.gate-row')[1]?.text()).toContain('覆盖 2 个入库码')
wrapper.unmount()
})
it('keeps different facts for the same gate code separate and labels the affected inbound', async () => {
vi.mocked(getContainerOutboundDispatch).mockResolvedValueOnce({
...outboundDetail,
aggregate: {
...outboundDetail.aggregate,
gate: {
passed: false,
checkedAt: '2026-08-26T12:00:00',
inbounds: [
{ inboundId: '201', inboundCode: 'IN-MISSING', gate: { passed: false, checkedAt: '2026-08-26T12:00:00', items: [{ code: 'CUSTOMS_CLEARANCE', name: '报关协同完成', passed: false, actual: 'MISSING', requirement: 'COMPLETED' }] } },
{ inboundId: '202', inboundCode: 'IN-PENDING', gate: { passed: false, checkedAt: '2026-08-26T12:00:00', items: [{ code: 'CUSTOMS_CLEARANCE', name: '报关协同完成', passed: false, actual: 'PENDING', requirement: 'COMPLETED' }] } },
{ inboundId: '203', inboundCode: 'IN-DONE', gate: { passed: true, checkedAt: '2026-08-26T12:00:00', items: [{ code: 'CUSTOMS_CLEARANCE', name: '报关协同完成', passed: true, actual: 'COMPLETED', requirement: 'COMPLETED' }] } },
],
},
},
} as never)
const wrapper = mountView(); await flushPromises()
expect(wrapper.findAll('.gate-row')).toHaveLength(2)
expect(wrapper.find('.gate-list').text()).toContain('MISSING')
expect(wrapper.find('.gate-list').text()).toContain('IN-MISSING')
expect(wrapper.find('.gate-list').text()).toContain('PENDING')
expect(wrapper.find('.gate-list').text()).toContain('IN-PENDING')
await wrapper.find('.gate-summary button').trigger('click')
expect(wrapper.findAll('.gate-row')).toHaveLength(3)
expect(wrapper.find('.gate-list').text()).toContain('IN-DONE')
wrapper.unmount()
})
it('keeps truck status as reference only and never counts the legacy truck gate as blocking', async () => {
vi.mocked(getContainerOutboundDispatch).mockResolvedValueOnce({
...outboundDetail,
aggregate: {
...outboundDetail.aggregate,
truckAppointmentStatus: 'ARRIVED',
gate: {
passed: false,
checkedAt: '2026-08-26T12:00:00',
inbounds: [{ inboundId: '301', inboundCode: 'IN-301', gate: { passed: false, checkedAt: '2026-08-26T12:00:00', items: [
{ code: 'TRUCK_DEPARTED', name: '关联拖车已离仓', passed: false, actual: 'ARRIVED', requirement: 'DEPARTED或COMPLETED' },
{ code: 'CUSTOMS_CLEARANCE', name: '报关协同完成', passed: true, actual: 'COMPLETED', requirement: 'COMPLETED' },
] } }],
},
},
} as never)
const wrapper = mountView(); await flushPromises()
expect(wrapper.find('.gate-summary').text()).toContain('总门禁 1')
expect(wrapper.find('.gate-summary').text()).toContain('阻断 0')
expect(wrapper.find('.gate-summary').text()).toContain('拖车状态(参考):已到仓')
expect(wrapper.text()).not.toContain('关联拖车已离仓')
expect(wrapper.find('.outbound-panel__actions button').text()).toContain('确认离仓')
wrapper.unmount()
})
}) })
@@ -13,7 +13,7 @@ import StatusBadge from '@/shared/components/StatusBadge.vue'
import { CarrierScheduleQueryPanel } from '@/modules/port-schedules' import { CarrierScheduleQueryPanel } from '@/modules/port-schedules'
import { uploadLoadingDeclaration } from '@/modules/loading-lists' import { uploadLoadingDeclaration } from '@/modules/loading-lists'
import { confirmContainerOutboundDispatch, getContainerOutboundDispatch } from '@/modules/outbound-dispatch/api/platformOutboundDispatch' import { confirmContainerOutboundDispatch, getContainerOutboundDispatch } from '@/modules/outbound-dispatch/api/platformOutboundDispatch'
import type { ContainerOutboundDispatchDetail } from '@/modules/outbound-dispatch/types' import type { ContainerOutboundDispatchDetail, ContainerOutboundGateItem } from '@/modules/outbound-dispatch/types'
import ContainerOverviewPanel from '../components/ContainerOverviewPanel.vue' import ContainerOverviewPanel from '../components/ContainerOverviewPanel.vue'
import { import {
cancelContainerException, cancelContainerException,
@@ -62,17 +62,37 @@ const outboundLoading = ref(false)
const outboundConfirming = ref(false) const outboundConfirming = ref(false)
const outboundDetail = ref<ContainerOutboundDispatchDetail | null>(null) const outboundDetail = ref<ContainerOutboundDispatchDetail | null>(null)
const outboundError = ref<ApiErrorDetails | null>(null) const outboundError = ref<ApiErrorDetails | null>(null)
const showPassedGates = ref(false)
const carrierRefreshKey = ref(0) const carrierRefreshKey = ref(0)
const selectedDocument = computed(() => overview.value?.documents.find((item) => item.documentType === documentType.value) || null) const selectedDocument = computed(() => overview.value?.documents.find((item) => item.documentType === documentType.value) || null)
const documentTitle = computed(() => ({ DELIVERY_ORDER: 'DO 资料', PICKUP_ORDER: 'PU 资料', CUSTOMS_DECLARATION: '国内出口报关附件' })[documentType.value]) const documentTitle = computed(() => ({ DELIVERY_ORDER: 'DO 资料', PICKUP_ORDER: 'PU 资料', CUSTOMS_DECLARATION: '国内出口报关附件' })[documentType.value])
const title = computed(() => detail.value?.containerNo || detail.value?.cabinetNo || '货柜详情') const title = computed(() => detail.value?.containerNo || detail.value?.cabinetNo || '货柜详情')
const exceptionTitle = computed(() => overview.value?.exception.active ? '取消货柜异常' : '标记货柜异常') const exceptionTitle = computed(() => overview.value?.exception.active ? '取消货柜异常' : '标记货柜异常')
const outboundGateRows = computed(() => (outboundDetail.value?.aggregate.gate.inbounds || []).flatMap((inbound) => type AggregatedGateRow = ContainerOutboundGateItem & { key: string; inboundCodes: string[] }
(inbound.gate.items || []).map((item) => ({ ...item, inboundId: inbound.inboundId, inboundCode: inbound.inboundCode })))) const outboundGateRows = computed<AggregatedGateRow[]>(() => {
const grouped = new Map<string, AggregatedGateRow>()
for (const inbound of outboundDetail.value?.aggregate.gate.inbounds || []) {
for (const item of inbound.gate.items || []) {
if (item.code === 'TRUCK_DEPARTED') continue
const key = JSON.stringify([item.code, item.passed, item.actual ?? null, item.requirement ?? null, item.name])
const existing = grouped.get(key)
if (existing) {
if (!existing.inboundCodes.includes(inbound.inboundCode)) existing.inboundCodes.push(inbound.inboundCode)
} else grouped.set(key, { ...item, key, inboundCodes: [inbound.inboundCode] })
}
}
return [...grouped.values()]
})
const visibleOutboundGateRows = computed(() => showPassedGates.value ? outboundGateRows.value : outboundGateRows.value.filter((row) => !row.passed))
const outboundGateSummary = computed(() => ({
total: outboundGateRows.value.length,
passed: outboundGateRows.value.filter((row) => row.passed).length,
blocked: outboundGateRows.value.filter((row) => !row.passed).length,
}))
const canConfirmDeparture = computed(() => canConfirmOutboundDispatch.value const canConfirmDeparture = computed(() => canConfirmOutboundDispatch.value
&& outboundDetail.value?.aggregate.dispatchStatus === 'READY_FOR_DISPATCH' && outboundDetail.value?.aggregate.dispatchStatus === 'READY_FOR_DISPATCH'
&& outboundDetail.value.aggregate.gate.passed) && outboundGateSummary.value.blocked === 0)
function statusText(status?: ContainerPlanStatus | null) { return status ? ({ WAIT_LOADING: '待装柜', EDITING: '预装柜中', LOADING: '拣货中', COMPLETED: '装柜完成' })[status] : '—' } function statusText(status?: ContainerPlanStatus | null) { return status ? ({ WAIT_LOADING: '待装柜', EDITING: '预装柜中', LOADING: '拣货中', COMPLETED: '装柜完成' })[status] : '—' }
function formatTime(value?: string | null) { return value?.replace('T', ' ').slice(0, 16) || '—' } function formatTime(value?: string | null) { return value?.replace('T', ' ').slice(0, 16) || '—' }
@@ -90,7 +110,7 @@ async function load() {
async function loadOutboundDispatch() { async function loadOutboundDispatch() {
if (!containerId.value || !canViewOutboundDispatch.value) return if (!containerId.value || !canViewOutboundDispatch.value) return
outboundLoading.value = true; outboundError.value = null outboundLoading.value = true; outboundError.value = null; showPassedGates.value = false
try { outboundDetail.value = await getContainerOutboundDispatch(containerId.value) } try { outboundDetail.value = await getContainerOutboundDispatch(containerId.value) }
catch (cause) { outboundDetail.value = null; outboundError.value = getApiErrorDetails(cause, '国内出仓门禁加载失败') } catch (cause) { outboundDetail.value = null; outboundError.value = getApiErrorDetails(cause, '国内出仓门禁加载失败') }
finally { outboundLoading.value = false } finally { outboundLoading.value = false }
@@ -123,6 +143,9 @@ async function confirmDeparture() {
function outboundStatusText(status?: string | null) { function outboundStatusText(status?: string | null) {
return ({ READY_FOR_DISPATCH: '待确认离仓', WAITING_FOR_LOADING: '已确认离仓' } as Record<string, string>)[status || ''] || status || '—' return ({ READY_FOR_DISPATCH: '待确认离仓', WAITING_FOR_LOADING: '已确认离仓' } as Record<string, string>)[status || ''] || status || '—'
} }
function truckReferenceText(status?: string | null) {
return ({ SCHEDULED: '已预约', ARRIVED: '已到仓', DEPARTED: '已离仓', COMPLETED: '已完成' } as Record<string, string>)[status || ''] || status || '未关联'
}
function openDocument(type: string) { function openDocument(type: string) {
if (type === 'DESTINATION_CLEARANCE') { if (canManageCustoms.value) void openClearance(); return } if (type === 'DESTINATION_CLEARANCE') { if (canManageCustoms.value) void openClearance(); return }
@@ -211,14 +234,17 @@ onMounted(async () => { await load(); if (route.query.action === 'documents') op
<LoadErrorState v-if="overviewError" compact title="履约摘要加载失败" :message="overviewError.message" :show-retry="false" /> <LoadErrorState v-if="overviewError" compact title="履约摘要加载失败" :message="overviewError.message" :show-retry="false" />
<ContainerOverviewPanel v-else-if="overview" :overview="overview" :can-manage-documents="canEdit" :can-manage-customs="canManageCustoms" :can-manage-exception="canEdit" :document-busy="documentDownloading" @upload-document="openDocument" @download-document="downloadDocument" @manage-exception="openException" /> <ContainerOverviewPanel v-else-if="overview" :overview="overview" :can-manage-documents="canEdit" :can-manage-customs="canManageCustoms" :can-manage-exception="canEdit" :document-busy="documentDownloading" @upload-document="openDocument" @download-document="downloadDocument" @manage-exception="openException" />
<section v-if="detail.planStatus === 'COMPLETED' && canViewOutboundDispatch" class="outbound-panel surface-panel" aria-label="国内出仓门禁"> <section v-if="detail.planStatus === 'COMPLETED' && canViewOutboundDispatch" class="outbound-panel surface-panel" aria-label="国内出仓门禁">
<header class="outbound-panel__header"><div><h2>国内出仓门禁</h2><p>按整柜核验全部入库绑定,仅确认一次离仓。</p></div><div v-if="outboundDetail" class="outbound-panel__actions"><el-tag :type="outboundDetail.aggregate.gate.passed ? 'success' : 'danger'">{{ outboundDetail.aggregate.gate.passed ? '门禁通过' : '门禁阻断' }}</el-tag><StatusBadge :status="outboundStatusText(outboundDetail.aggregate.dispatchStatus)" /><el-button v-if="canConfirmDeparture" type="primary" :loading="outboundConfirming" @click="confirmDeparture">确认离仓</el-button></div></header> <header class="outbound-panel__header"><div><h2>国内出仓门禁</h2><p>按整柜核验全部入库绑定,仅确认一次离仓。</p></div><div v-if="outboundDetail" class="outbound-panel__actions"><el-tag :type="outboundGateSummary.blocked === 0 ? 'success' : 'danger'">{{ outboundGateSummary.blocked === 0 ? '门禁通过' : '门禁阻断' }}</el-tag><StatusBadge :status="outboundStatusText(outboundDetail.aggregate.dispatchStatus)" /><el-button v-if="canConfirmDeparture" type="primary" :loading="outboundConfirming" @click="confirmDeparture">确认离仓</el-button></div></header>
<LoadErrorState v-if="outboundError" compact title="国内出仓操作失败" :message="outboundError.message" :loading="outboundLoading" @retry="loadOutboundDispatch"><template #details><ApiErrorMeta :details="outboundError" /></template></LoadErrorState> <LoadErrorState v-if="outboundError" compact title="国内出仓操作失败" :message="outboundError.message" :loading="outboundLoading" @retry="loadOutboundDispatch"><template #details><ApiErrorMeta :details="outboundError" /></template></LoadErrorState>
<div v-else-if="outboundLoading" class="outbound-panel__loading"><el-skeleton :rows="2" animated /></div> <div v-else-if="outboundLoading" class="outbound-panel__loading"><el-skeleton :rows="2" animated /></div>
<template v-else-if="outboundDetail"> <template v-else-if="outboundDetail">
<p v-if="!outboundDetail.aggregate.gate.passed" class="outbound-panel__hint">以下任一阻断项处理完成后,重新加载门禁即可确认离仓。</p> <div class="gate-summary"><span>总门禁 <strong>{{ outboundGateSummary.total }}</strong></span><span class="gate-summary__passed">已通过 <strong>{{ outboundGateSummary.passed }}</strong></span><span :class="{ 'gate-summary__blocked': outboundGateSummary.blocked }">阻断 <strong>{{ outboundGateSummary.blocked }}</strong></span><span class="gate-summary__reference">拖车状态(参考):{{ truckReferenceText(outboundDetail.aggregate.truckAppointmentStatus) }}</span><el-button v-if="outboundGateSummary.passed" link type="primary" @click="showPassedGates = !showPassedGates">{{ showPassedGates ? '收起已通过项' : `查看全部门禁(${outboundGateSummary.total})` }}</el-button></div>
<p v-if="outboundGateSummary.blocked" class="outbound-panel__hint">默认仅展示阻断项;处理完成后重新加载门禁即可确认离仓。</p>
<p v-else-if="!showPassedGates" class="outbound-panel__hint outbound-panel__hint--success">全部门禁已通过,可确认整柜离仓;展开可查看去重后的完整门禁。</p>
<div class="gate-list"> <div class="gate-list">
<article v-for="row in outboundGateRows" :key="`${row.inboundId}-${row.code}`" class="gate-row"> <article v-for="row in visibleOutboundGateRows" :key="row.key" class="gate-row">
<el-tag size="small" :type="row.passed ? 'success' : 'danger'">{{ row.passed ? '通过' : '阻断' }}</el-tag><strong>{{ row.name }}</strong><span>{{ row.inboundCode }}</span><span>{{ row.actual || '暂无事实' }}</span><small>要求:{{ row.requirement || '—' }}</small> <el-tag size="small" :type="row.passed ? 'success' : 'danger'">{{ row.passed ? '通过' : '阻断' }}</el-tag><strong>{{ row.name }}</strong><span class="gate-row__actual">{{ row.actual || '暂无事实' }}</span><small>要求:{{ row.requirement || '—' }}</small>
<div class="gate-row__coverage"><span>{{ row.passed ? '覆盖' : '影响' }} {{ row.inboundCodes.length }} 个入库码</span><el-tag v-for="code in row.inboundCodes.slice(0, 3)" :key="code" size="small" effect="plain">{{ code }}</el-tag><span v-if="row.inboundCodes.length > 3">+{{ row.inboundCodes.length - 3 }}</span></div>
</article> </article>
</div> </div>
</template> </template>
@@ -233,5 +259,5 @@ onMounted(async () => { await load(); if (route.query.action === 'documents') op
</template> </template>
<style scoped> <style scoped>
.container-detail-page { display:grid; min-width:0; gap:12px; }.container-overview { overflow:hidden; }.container-overview__header { display:grid; grid-template-columns:auto minmax(0,1fr) auto; align-items:center; gap:14px; padding:10px 16px; }.container-overview__header h1 { margin:0; font-size:18px; }.container-overview__header p { margin:2px 0 0; color:var(--cc-muted); font-size:12px; }.container-overview__summary { display:grid; grid-template-columns:repeat(5,minmax(0,1fr)); border-top:1px solid var(--cc-border); }.container-overview__summary>div { display:flex; min-width:0; flex-direction:column; gap:3px; padding:8px 14px; border-right:1px solid var(--cc-border); }.container-overview__summary>div:last-child { border-right:0; }.container-overview__summary span,dt { color:var(--cc-muted); font-size:12px; }.container-overview__summary strong,dd { overflow:hidden; margin:0; text-overflow:ellipsis; white-space:nowrap; }.detail-fields { padding:12px 16px; }.detail-fields h2 { margin:0 0 10px; font-size:15px; }.detail-fields dl { display:grid; grid-template-columns:repeat(6,minmax(0,1fr)); gap:9px 16px; margin:0; }.detail-fields dl>div { min-width:0; }.detail-fields dd { margin-top:3px; font-size:13px; }.outbound-panel{padding:12px 16px}.outbound-panel__header{display:flex;align-items:center;justify-content:space-between;gap:12px}.outbound-panel__header h2{margin:0;font-size:15px}.outbound-panel__header p,.outbound-panel__hint{margin:3px 0 0;color:var(--cc-muted);font-size:12px}.outbound-panel__actions{display:flex;align-items:center;gap:8px}.outbound-panel__loading{padding:8px 0}.gate-list{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:6px 12px;margin-top:10px}.gate-row{display:grid;grid-template-columns:auto minmax(110px,.8fr) minmax(85px,.6fr) minmax(100px,1fr);align-items:center;gap:8px;min-width:0;padding:7px 8px;border:1px solid var(--cc-border);border-radius:6px;background:var(--cc-surface-muted)}.gate-row span,.gate-row small{overflow:hidden;color:var(--cc-muted);font-size:12px;text-overflow:ellipsis;white-space:nowrap}.gate-row small{grid-column:2/-1}.carrier-panel{padding:12px 16px}@media(max-width:1200px){.detail-fields dl{grid-template-columns:repeat(4,minmax(0,1fr))}.gate-list{grid-template-columns:1fr}}@media(max-width:720px){.container-overview__header{grid-template-columns:auto minmax(0,1fr)}.container-overview__header>:last-child{grid-column:2}.container-overview__summary,.detail-fields dl{grid-template-columns:repeat(2,minmax(0,1fr))}.outbound-panel__header{align-items:flex-start;flex-direction:column}.outbound-panel__actions{flex-wrap:wrap}.gate-row{grid-template-columns:auto minmax(0,1fr)}.gate-row span,.gate-row small{grid-column:2}} .container-detail-page { display:grid; min-width:0; gap:12px; }.container-overview { overflow:hidden; }.container-overview__header { display:grid; grid-template-columns:auto minmax(0,1fr) auto; align-items:center; gap:14px; padding:10px 16px; }.container-overview__header h1 { margin:0; font-size:18px; }.container-overview__header p { margin:2px 0 0; color:var(--cc-muted); font-size:12px; }.container-overview__summary { display:grid; grid-template-columns:repeat(5,minmax(0,1fr)); border-top:1px solid var(--cc-border); }.container-overview__summary>div { display:flex; min-width:0; flex-direction:column; gap:3px; padding:8px 14px; border-right:1px solid var(--cc-border); }.container-overview__summary>div:last-child { border-right:0; }.container-overview__summary span,dt { color:var(--cc-muted); font-size:12px; }.container-overview__summary strong,dd { overflow:hidden; margin:0; text-overflow:ellipsis; white-space:nowrap; }.detail-fields { padding:12px 16px; }.detail-fields h2 { margin:0 0 10px; font-size:15px; }.detail-fields dl { display:grid; grid-template-columns:repeat(6,minmax(0,1fr)); gap:9px 16px; margin:0; }.detail-fields dl>div { min-width:0; }.detail-fields dd { margin-top:3px; font-size:13px; }.outbound-panel{padding:12px 16px}.outbound-panel__header{display:flex;align-items:center;justify-content:space-between;gap:12px}.outbound-panel__header h2{margin:0;font-size:15px}.outbound-panel__header p,.outbound-panel__hint{margin:3px 0 0;color:var(--cc-muted);font-size:12px}.outbound-panel__hint--success{color:var(--el-color-success)}.outbound-panel__actions{display:flex;align-items:center;gap:8px}.outbound-panel__loading{padding:8px 0}.gate-summary{display:flex;align-items:center;gap:14px;margin-top:8px;color:var(--cc-muted);font-size:12px}.gate-summary__passed strong{color:var(--el-color-success)}.gate-summary__blocked strong{color:var(--el-color-danger)}.gate-summary__reference{margin-left:auto;padding-left:10px;border-left:1px solid var(--cc-border)}.gate-list{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:6px 12px;margin-top:7px}.gate-row{display:grid;grid-template-columns:auto minmax(130px,.9fr) minmax(100px,1fr);align-items:center;gap:6px 8px;min-width:0;padding:6px 8px;border:1px solid var(--cc-border);border-radius:6px;background:var(--cc-surface-muted)}.gate-row__actual,.gate-row small{overflow:hidden;color:var(--cc-muted);font-size:12px;text-overflow:ellipsis;white-space:nowrap}.gate-row small{grid-column:2/-1}.gate-row__coverage{display:flex;grid-column:2/-1;min-width:0;align-items:center;gap:5px;overflow:hidden}.gate-row__coverage>span{flex:0 0 auto;color:var(--cc-muted);font-size:11px}.gate-row__coverage :deep(.el-tag){max-width:120px;overflow:hidden;text-overflow:ellipsis}.carrier-panel{padding:12px 16px}@media(max-width:1200px){.detail-fields dl{grid-template-columns:repeat(4,minmax(0,1fr))}.gate-list{grid-template-columns:1fr}}@media(max-width:720px){.container-overview__header{grid-template-columns:auto minmax(0,1fr)}.container-overview__header>:last-child{grid-column:2}.container-overview__summary,.detail-fields dl{grid-template-columns:repeat(2,minmax(0,1fr))}.outbound-panel__header{align-items:flex-start;flex-direction:column}.outbound-panel__actions,.gate-summary{flex-wrap:wrap}.gate-summary__reference{width:100%;margin-left:0;padding-left:0;border-left:0}.gate-row{grid-template-columns:auto minmax(0,1fr)}.gate-row__actual,.gate-row small,.gate-row__coverage{grid-column:2}}
</style> </style>