This commit is contained in:
2026-09-14 18:34:34 +08:00
parent 25825685c3
commit 66f49b97e5
40 changed files with 472 additions and 304 deletions
@@ -1,7 +1,8 @@
<script setup lang="ts">
import '@/styles/adminDensity.css'
import { computed, onMounted, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ArrowLeft, ClipboardList, FileUp, Link2, Package, Receipt, Wallet } from '@lucide/vue'
import { ArrowLeft, FileUp } from '@lucide/vue'
import { ElMessage, ElMessageBox, type UploadFile } from 'element-plus'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
@@ -350,7 +351,7 @@ async function confirmDeparture() {
if (!aggregate || !canConfirmDeparture.value || outboundConfirming.value) return
outboundConfirming.value = true
try {
await ElMessageBox.confirm('确认整柜已离开国内仓?系统将再次校验柜内全部入库绑定,成功后自动创建船节点查询任务。', '确认国内出仓', { type: 'warning', confirmButtonText: '确认离仓' })
await ElMessageBox.confirm('确认整柜已离开国内仓?系统将再次校验柜内全部入库绑定,成功后自动创建船节点查询任务。', '确认国内出仓', { customClass: 'admin-density-confirm', type: 'warning', confirmButtonText: '确认离仓' })
} catch { outboundConfirming.value = false; return }
outboundError.value = null
try {
@@ -515,31 +516,31 @@ onMounted(async () => { await load(); if (isOverseas.value && route.query.action
</script>
<template>
<AdminPage class="container-detail-page">
<AdminPage class="container-detail-page admin-density">
<section class="container-overview surface-panel">
<header class="container-overview__header"><el-button link type="primary" @click="goBack"><ArrowLeft :size="16" />返回列表</el-button><div><h1>{{ title }}</h1><p>{{ isDomestic ? '送柜、装柜、出口报关与国内出仓' : '海运节点、目的港清关、提柜与还空' }}</p></div><StatusBadge v-if="detail" :status="primaryStatusText" /></header>
<header class="container-overview__header"><el-button link class="density-back" type="primary" @click="goBack"><ArrowLeft :size="16" />返回列表</el-button><div><h1>{{ title }}</h1><p>{{ isDomestic ? '送柜、装柜、出口报关与国内出仓' : '海运节点、目的港清关、提柜与还空' }}</p></div><StatusBadge v-if="detail" :status="primaryStatusText" /></header>
<div v-if="detail" class="container-overview__summary"><div><span>柜次号</span><strong>{{ detail.cabinetNo || '—' }}</strong></div><div><span>集装箱号</span><strong>{{ detail.containerNo || '待补录' }}</strong></div><div><span>{{ isDomestic ? '装柜清单号' : '当前海外阶段' }}</span><strong>{{ isDomestic ? (detail.planNo || '—') : primaryStatusText }}</strong></div><div><span>{{ isDomestic ? '柜型' : '起运仓' }}</span><strong>{{ isDomestic ? (detail.containerType || '—') : (detail.warehouseName || '—') }}</strong></div><div><span>{{ isDomestic ? '装柜仓库' : '目的仓 / 接柜仓' }}</span><strong :title="isDomestic ? detail.warehouseName || '—' : overseasDestinationText">{{ isDomestic ? (detail.warehouseName || '—') : overseasDestinationText }}</strong></div></div>
<div v-if="isOverseas && detail" class="overseas-stage-strip" :class="{ 'is-overdue': workbenchDetail?.terminalDrayage?.emptyReturnWarning.status === 'OVERDUE' }"><div><span>当前海外履约状态</span><strong>{{ primaryStatusText }}</strong></div><p>{{ emptyReturnWarningText }}</p></div>
</section>
<LoadErrorState v-if="error" title="货柜详情加载失败" :message="error.message" :loading="loading" @retry="load"><template #details><ApiErrorMeta :details="error" /></template></LoadErrorState>
<template v-else-if="detail">
<el-tabs v-model="activeTab" class="workbench-tabs" @tab-change="rememberWorkbenchTab">
<el-tab-pane :label="isDomestic ? '送柜报关' : '清关提柜'" name="overview"><template #label><span class="workbench-tab-label"><ClipboardList :size="18" aria-hidden="true" /><span>{{ isDomestic ? '送柜报关' : '清关提柜' }}</span></span></template></el-tab-pane>
<el-tab-pane v-if="canViewCurrentLoadingList" label="装柜清单" name="loading-list"><template #label><span class="workbench-tab-label"><Package :size="18" aria-hidden="true" /><span>装柜清单</span></span></template></el-tab-pane>
<el-tab-pane v-if="canViewFirstLegFees" label="国内段费用(头程)" name="first-leg-fees"><template #label><span class="workbench-tab-label"><Receipt :size="18" aria-hidden="true" /><span>国内段费用(头程)</span></span></template></el-tab-pane>
<el-tab-pane v-if="canViewLastLegFees" label="目的港及海外段费用(尾程)" name="last-leg-fees"><template #label><span class="workbench-tab-label"><Wallet :size="18" aria-hidden="true" /><span>目的港及海外段费用(尾程)</span></span></template></el-tab-pane>
<el-tabs v-model="activeTab" class="workbench-tabs density-element-tabs" @tab-change="rememberWorkbenchTab">
<el-tab-pane :label="isDomestic ? '送柜报关' : '清关提柜'" name="overview"><template #label><span class="workbench-tab-label"><span class="density-tab-icon" aria-hidden="true">📋</span><span>{{ isDomestic ? '送柜报关' : '清关提柜' }}</span></span></template></el-tab-pane>
<el-tab-pane v-if="canViewCurrentLoadingList" label="装柜清单" name="loading-list"><template #label><span class="workbench-tab-label"><span class="density-tab-icon" aria-hidden="true">📦</span><span>装柜清单</span></span></template></el-tab-pane>
<el-tab-pane v-if="canViewFirstLegFees" label="国内段费用(头程)" name="first-leg-fees"><template #label><span class="workbench-tab-label"><span class="density-tab-icon" aria-hidden="true">💰</span><span>国内段费用(头程)</span></span></template></el-tab-pane>
<el-tab-pane v-if="canViewLastLegFees" label="目的港及海外段费用(尾程)" name="last-leg-fees"><template #label><span class="workbench-tab-label"><span class="density-tab-icon" aria-hidden="true">💰</span><span>目的港及海外段费用(尾程)</span></span></template></el-tab-pane>
</el-tabs>
<LoadErrorState v-if="overviewError" compact title="履约摘要加载失败" :message="overviewError.message" :show-retry="false" />
<section v-show="activeTab === 'overview'" class="detail-section surface-panel" aria-labelledby="container-information-heading">
<header class="detail-section__heading"><h2 id="container-information-heading">货柜信息</h2><PalletLabelPrintDialog v-if="overview?.loadingPlanId && detail.warehouseId" :plan-id="overview.loadingPlanId" :warehouse-id="detail.warehouseId" :disabled="!detail.containerNo || detail.planStatus === 'EDITING'" /></header>
<div class="detail-fields"><dl><div><dt>真实订舱</dt><dd><span>{{ detail.oceanBookingId ? (detail.billOfLadingNo || '已关联') : '未关联' }}</span><el-button v-if="canLinkOceanBooking" link type="primary" @click="openOceanBookingLink"><Link2 :size="14" />关联已放舱订舱</el-button></dd></div><div><dt>计划渠道</dt><dd>{{ plannedChannelName }}</dd></div><div><dt>实际渠道</dt><dd>{{ detail.channelName || '待关联订舱' }}</dd></div><div><dt>异常状态</dt><dd>{{ detail.exceptionActive ? '异常' : '正常' }}</dd></div><div><dt>目的仓</dt><dd>{{ overseasDestinationText }}</dd></div><div><dt>拖车公司</dt><dd>{{ detail.terminalTruckingCompany || workbenchDetail?.terminalDrayage?.fleetSupplierName || '—' }}</dd></div></dl></div>
<header class="detail-section__heading"><h2 id="container-information-heading">货柜信息</h2><PalletLabelPrintDialog dense v-if="overview?.loadingPlanId && detail.warehouseId" :plan-id="overview.loadingPlanId" :warehouse-id="detail.warehouseId" :disabled="!detail.containerNo || detail.planStatus === 'EDITING'" /></header>
<div class="detail-fields"><dl class="density-fields"><div><dt>真实订舱</dt><dd><span>{{ detail.oceanBookingId ? (detail.billOfLadingNo || '已关联') : '未关联' }}</span><el-button v-if="canLinkOceanBooking" class="density-action" data-tone="primary" @click="openOceanBookingLink">关联已放舱订舱</el-button></dd></div><div><dt>计划渠道</dt><dd>{{ plannedChannelName }}</dd></div><div><dt>实际渠道</dt><dd>{{ detail.channelName || '待关联订舱' }}</dd></div><div><dt>异常状态</dt><dd>{{ detail.exceptionActive ? '异常' : '正常' }}</dd></div><div><dt>目的仓</dt><dd>{{ overseasDestinationText }}</dd></div><div><dt>拖车公司</dt><dd>{{ detail.terminalTruckingCompany || workbenchDetail?.terminalDrayage?.fleetSupplierName || '—' }}</dd></div></dl></div>
<ContainerOverviewPanel v-if="isDomestic && overview" :overview="overview" :sections="['exception']" :can-manage-documents="canEdit" :can-manage-customs="canManageCustoms" :can-manage-exception="canEdit" @manage-exception="openException" />
</section>
<section v-show="activeTab === 'overview'" class="detail-section surface-panel" aria-labelledby="container-voyage-heading">
<h2 id="container-voyage-heading">船名航次信息</h2>
<div class="detail-fields"><dl><div><dt>船公司</dt><dd>{{ detail.shippingCompanyName || '—' }}</dd></div><div><dt>船名 / 航次</dt><dd>{{ [detail.vesselName, detail.voyageNo].filter(Boolean).join(' / ') || '—' }}</dd></div><div><dt>预计开船</dt><dd>{{ formatTime(detail.estimatedSailingAt) }}</dd></div><div><dt>预计到港</dt><dd>{{ formatTime(detail.estimatedPortArrivalAt) }}</dd></div></dl></div>
<div class="carrier-panel"><CarrierScheduleQueryPanel :key="carrierRefreshKey" :ocean-booking-id="detail.oceanBookingId" :container-booking-id="detail.id" :warehouse-timeline="overview?.timeline || []" /></div>
<div class="detail-fields"><dl class="density-fields"><div><dt>船公司</dt><dd>{{ detail.shippingCompanyName || '—' }}</dd></div><div><dt>船名 / 航次</dt><dd>{{ [detail.vesselName, detail.voyageNo].filter(Boolean).join(' / ') || '—' }}</dd></div><div><dt>预计开船</dt><dd>{{ formatTime(detail.estimatedSailingAt) }}</dd></div><div><dt>预计到港</dt><dd>{{ formatTime(detail.estimatedPortArrivalAt) }}</dd></div></dl></div>
<div class="carrier-panel"><CarrierScheduleQueryPanel dense :key="carrierRefreshKey" :ocean-booking-id="detail.oceanBookingId" :container-booking-id="detail.id" :warehouse-timeline="overview?.timeline || []" /></div>
</section>
<section v-show="activeTab === 'overview'" class="detail-section surface-panel" aria-labelledby="container-customs-heading">
@@ -572,14 +573,14 @@ onMounted(async () => { await load(); if (isOverseas.value && route.query.action
<section v-if="isOverseas" v-show="activeTab === 'overview'" class="detail-section surface-panel" aria-labelledby="container-overseas-receiving-heading">
<header class="detail-section__heading"><h2 id="container-overseas-receiving-heading">提柜、卸柜与还空</h2><el-button v-if="canRecordEmptyReturn" type="primary" :loading="emptyReturnSaving" @click="openEmptyReturn">确认还柜</el-button></header>
<div class="detail-fields"><dl><div><dt>码头拖车状态</dt><dd>{{ workbenchDetail?.statusSummary?.terminalDrayageStatus || '—' }}</dd></div><div><dt>码头车队</dt><dd>{{ workbenchDetail?.terminalDrayage?.fleetSupplierName || '—' }}</dd></div><div><dt>预约提柜</dt><dd>{{ formatTime(workbenchDetail?.terminalDrayage?.pickupAppointmentAt) }}</dd></div><div><dt>实际提柜</dt><dd>{{ formatTime(workbenchDetail?.terminalDrayage?.actualPickupAt) }}</dd></div><div><dt>卸柜完成</dt><dd>{{ formatTime((workbenchDetail?.actualFacts?.unloadingCompletedAt || workbenchDetail?.terminalDrayage?.unloadingCompletedAt) as string | null) }}</dd></div><div><dt>实际还柜</dt><dd>{{ formatTime(workbenchDetail?.terminalDrayage?.actualEmptyReturnAt) }}</dd></div></dl></div>
<div class="detail-fields"><dl class="density-fields"><div><dt>码头拖车状态</dt><dd>{{ workbenchDetail?.statusSummary?.terminalDrayageStatus || '—' }}</dd></div><div><dt>码头车队</dt><dd>{{ workbenchDetail?.terminalDrayage?.fleetSupplierName || '—' }}</dd></div><div><dt>预约提柜</dt><dd>{{ formatTime(workbenchDetail?.terminalDrayage?.pickupAppointmentAt) }}</dd></div><div><dt>实际提柜</dt><dd>{{ formatTime(workbenchDetail?.terminalDrayage?.actualPickupAt) }}</dd></div><div><dt>卸柜完成</dt><dd>{{ formatTime((workbenchDetail?.actualFacts?.unloadingCompletedAt || workbenchDetail?.terminalDrayage?.unloadingCompletedAt) as string | null) }}</dd></div><div><dt>实际还柜</dt><dd>{{ formatTime(workbenchDetail?.terminalDrayage?.actualEmptyReturnAt) }}</dd></div></dl></div>
</section>
<section v-if="isDomestic && activeTab === 'loading-list'" class="detail-section surface-panel" aria-labelledby="container-loading-list-heading">
<header class="loading-list-heading"><div><h2 id="container-loading-list-heading">{{ loadingListHeading }}</h2><p>{{ loadingListDescription }}</p></div><el-button v-if="loadingDetail && loadingListMode === 'confirmed'" @click="printLoadingList">打印装柜清单</el-button></header>
<ContainerOverviewPanel v-if="overview" :overview="overview" :sections="['pda']" :can-manage-documents="false" :can-manage-customs="false" :can-manage-exception="false" />
<ContainerCustomsPanel v-if="overview?.loadingPlanId && canListCustoms" :loading-plan-id="overview.loadingPlanId" :can-list="canListCustoms" :can-manage="canManageCustoms" :departure-state="customsDepartureState" @refreshed="refreshAfterCustoms" />
<LoadingListDetailDrawer :mode="loadingListMode" embedded :loading="loadingDetailLoading" :detail="loadingDetail" :error="loadingDetailError" :can-edit="false" @back="activeTab = 'overview'" @retry="overview && loadLoadingDetail(detail, overview)" />
<LoadingListDetailDrawer dense :mode="loadingListMode" embedded :loading="loadingDetailLoading" :detail="loadingDetail" :error="loadingDetailError" :can-edit="false" @back="activeTab = 'overview'" @retry="overview && loadLoadingDetail(detail, overview)" />
</section>
<section v-if="canViewFirstLegFees && firstLegFeesOpened" v-show="activeTab === 'first-leg-fees'" v-loading="loadingDetailLoading" class="detail-section surface-panel" aria-label="头程计费">
@@ -594,24 +595,17 @@ onMounted(async () => { await load(); if (isOverseas.value && route.query.action
</template>
<OceanBookingLinkDialog v-model="oceanBookingLinkOpen" :container="detail" @linked="load" />
<AppDialog v-model="emptyReturnOpen" title="确认还柜" width="min(520px, calc(100vw - 28px))" confirm-text="确认还柜" :confirm-loading="emptyReturnSaving" :confirm-disabled="!emptyReturnAt || !canRecordEmptyReturn" @confirm="confirmEmptyReturn"><el-alert title="提交后货柜将进入已还柜状态,请核对实际还柜时间。" type="warning" :closable="false" show-icon /><el-form label-position="top"><el-form-item label="实际还柜时间" required><el-date-picker v-model="emptyReturnAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="请选择实际还柜时间" /></el-form-item><el-form-item label="备注"><el-input v-model="emptyReturnReason" type="textarea" :rows="3" maxlength="500" show-word-limit placeholder="请输入备注(选填)" /></el-form-item></el-form><LoadErrorState v-if="emptyReturnError" compact title="确认还柜失败" :message="emptyReturnError.message" :show-retry="false"><template #details><ApiErrorMeta :details="emptyReturnError" /></template></LoadErrorState></AppDialog>
<AppDialog v-model="documentOpen" :title="documentTitle" width="min(520px, calc(100vw - 28px))" confirm-text="上传资料" :confirm-loading="documentSaving" :confirm-disabled="!documentFile" @confirm="saveDocument"><el-form label-position="top"><el-form-item :label="documentTitle"><el-upload :key="documentUploadKey" :auto-upload="false" :limit="1" :on-change="selectDocumentFile" :on-remove="removeDocumentFile"><el-button><FileUp :size="16" />选择文件</el-button><template #tip><div class="el-upload__tip">{{ selectedDocument ? '上传新文件将替换当前资料并保留历史。' : `请选择需要提交的${documentTitle}。` }}</div></template></el-upload></el-form-item></el-form><LoadErrorState v-if="documentError" compact title="资料上传失败" :message="documentError.message" :show-retry="false"><template #details><ApiErrorMeta :details="documentError" /></template></LoadErrorState></AppDialog>
<AppDialog v-model="exceptionOpen" :title="exceptionTitle" width="min(520px, calc(100vw - 28px))" :confirm-text="overview?.exception.active ? '确认取消' : '确认标记'" :confirm-loading="exceptionSaving" :confirm-disabled="!exceptionReason.trim()" @confirm="saveException"><el-alert :title="overview?.exception.active ? '取消备注为必填项,提交后将保留历史记录。' : '异常原因将同步记录在货柜详情和操作日志中。'" :type="overview?.exception.active ? 'warning' : 'error'" :closable="false" /><el-form label-position="top"><el-form-item :label="overview?.exception.active ? '取消备注' : '异常原因'" required><el-input v-model="exceptionReason" type="textarea" :rows="4" maxlength="500" show-word-limit placeholder="请输入异常原因或取消备注" /></el-form-item></el-form><LoadErrorState v-if="exceptionError" compact title="异常处理失败" :message="exceptionError.message" :show-retry="false" /></AppDialog>
<AppDialog v-model="clearanceOpen" title="目的港清关资料" width="min(520px, calc(100vw - 28px))" :loading="clearanceLoading" :show-confirm="!clearanceDocument?.completedAt" confirm-text="上传资料" :confirm-loading="clearanceSaving" :confirm-disabled="!clearanceFile" @confirm="uploadClearance"><el-alert v-if="clearanceDocument?.completedAt" type="success" :closable="false" :title="`目的港清关已完成:${clearanceDocument.fileName}`" /><template v-else><el-form label-position="top"><el-form-item label="目的港清关资料"><el-upload :auto-upload="false" :limit="1" :on-change="selectClearanceFile" :on-remove="removeClearanceFile"><el-button><FileUp :size="16" />选择文件</el-button></el-upload></el-form-item></el-form><el-button v-if="clearanceDocument" type="success" plain :loading="clearanceSaving" @click="completeClearance">确认目的港清关完成</el-button></template><LoadErrorState v-if="clearanceError" compact title="目的港清关资料操作失败" :message="clearanceError.message" :show-retry="false"><template #details><ApiErrorMeta :details="clearanceError" /></template></LoadErrorState></AppDialog>
<AppDialog class="admin-density-dialog" v-model="emptyReturnOpen" title="确认还柜" width="min(520px, calc(100vw - 28px))" confirm-text="确认还柜" :confirm-loading="emptyReturnSaving" :confirm-disabled="!emptyReturnAt || !canRecordEmptyReturn" @confirm="confirmEmptyReturn"><el-alert title="提交后货柜将进入已还柜状态,请核对实际还柜时间。" type="warning" :closable="false" show-icon /><el-form class="density-form" label-position="top"><el-form-item label="实际还柜时间" required><el-date-picker v-model="emptyReturnAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="请选择实际还柜时间" /></el-form-item><el-form-item label="备注"><el-input v-model="emptyReturnReason" type="textarea" :rows="3" maxlength="500" show-word-limit placeholder="请输入备注(选填)" /></el-form-item></el-form><LoadErrorState v-if="emptyReturnError" compact title="确认还柜失败" :message="emptyReturnError.message" :show-retry="false"><template #details><ApiErrorMeta :details="emptyReturnError" /></template></LoadErrorState></AppDialog>
<AppDialog class="admin-density-dialog" v-model="documentOpen" :title="documentTitle" width="min(520px, calc(100vw - 28px))" confirm-text="上传资料" :confirm-loading="documentSaving" :confirm-disabled="!documentFile" @confirm="saveDocument"><el-form class="density-form" label-position="top"><el-form-item :label="documentTitle"><el-upload :key="documentUploadKey" :auto-upload="false" :limit="1" :on-change="selectDocumentFile" :on-remove="removeDocumentFile"><el-button><FileUp :size="16" />选择文件</el-button><template #tip><div class="el-upload__tip">{{ selectedDocument ? '上传新文件将替换当前资料并保留历史。' : `请选择需要提交的${documentTitle}。` }}</div></template></el-upload></el-form-item></el-form><LoadErrorState v-if="documentError" compact title="资料上传失败" :message="documentError.message" :show-retry="false"><template #details><ApiErrorMeta :details="documentError" /></template></LoadErrorState></AppDialog>
<AppDialog class="admin-density-dialog" v-model="exceptionOpen" :title="exceptionTitle" width="min(520px, calc(100vw - 28px))" :confirm-text="overview?.exception.active ? '确认取消' : '确认标记'" :confirm-loading="exceptionSaving" :confirm-disabled="!exceptionReason.trim()" @confirm="saveException"><el-alert :title="overview?.exception.active ? '取消备注为必填项,提交后将保留历史记录。' : '异常原因将同步记录在货柜详情和操作日志中。'" :type="overview?.exception.active ? 'warning' : 'error'" :closable="false" /><el-form class="density-form" label-position="top"><el-form-item :label="overview?.exception.active ? '取消备注' : '异常原因'" required><el-input v-model="exceptionReason" type="textarea" :rows="4" maxlength="500" show-word-limit placeholder="请输入异常原因或取消备注" /></el-form-item></el-form><LoadErrorState v-if="exceptionError" compact title="异常处理失败" :message="exceptionError.message" :show-retry="false" /></AppDialog>
<AppDialog class="admin-density-dialog" v-model="clearanceOpen" title="目的港清关资料" width="min(520px, calc(100vw - 28px))" :loading="clearanceLoading" :show-confirm="!clearanceDocument?.completedAt" confirm-text="上传资料" :confirm-loading="clearanceSaving" :confirm-disabled="!clearanceFile" @confirm="uploadClearance"><el-alert v-if="clearanceDocument?.completedAt" type="success" :closable="false" :title="`目的港清关已完成:${clearanceDocument.fileName}`" /><template v-else><el-form class="density-form" label-position="top"><el-form-item label="目的港清关资料"><el-upload :auto-upload="false" :limit="1" :on-change="selectClearanceFile" :on-remove="removeClearanceFile"><el-button><FileUp :size="16" />选择文件</el-button></el-upload></el-form-item></el-form><el-button v-if="clearanceDocument" type="success" plain :loading="clearanceSaving" @click="completeClearance">确认目的港清关完成</el-button></template><LoadErrorState v-if="clearanceError" compact title="目的港清关资料操作失败" :message="clearanceError.message" :show-retry="false"><template #details><ApiErrorMeta :details="clearanceError" /></template></LoadErrorState></AppDialog>
</AdminPage>
</template>
<style scoped>
.workbench-tabs { min-width:0; overflow:hidden; border-bottom:1px solid rgba(228,228,231,.5); border-radius:20px 20px 0 0; background:rgba(255,255,255,.95); box-shadow:0 -4px 16px rgba(0,0,0,.04); }
.workbench-tabs :deep(.el-tabs__header) { margin:0; }
.workbench-tabs :deep(.el-tabs__nav-wrap::after), .workbench-tabs :deep(.el-tabs__active-bar), .workbench-tabs :deep(.el-tabs__content) { display:none; }
.workbench-tabs :deep(.el-tabs__item) { height:auto; padding:16px 28px !important; border-bottom:3px solid transparent; color:#71717a; font-size:15px; font-weight:600; transition:background-color .2s, color .2s, border-color .2s; }
.workbench-tabs :deep(.el-tabs__item:hover) { background:rgba(102,126,234,.05); color:#667eea; }
.workbench-tabs :deep(.el-tabs__item.is-active) { border-bottom-color:#667eea; background:rgba(102,126,234,.08); color:#667eea; font-weight:800; }
.workbench-tabs :deep(.el-tabs__item:focus-visible) { outline:2px solid #667eea; outline-offset:-4px; }
.workbench-tab-label { display:inline-flex; align-items:center; justify-content:center; gap:8px; white-space:nowrap; }
.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-section{display:grid;min-width:0;gap:12px;padding:12px 16px}.detail-section>h2{margin:0;font-size:15px}.detail-fields { padding:0; }.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 0 0;border-top:1px solid var(--cc-border)}.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-top:4px}@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}}
.detail-section__heading { display:flex; align-items:center; justify-content:space-between; gap:12px; }
.detail-section__heading h2 { margin:0; font-size:15px; }
.overseas-stage-strip{display:flex;align-items:center;justify-content:space-between;gap:18px;padding:9px 16px;border-top:1px solid var(--cc-border);background:#f5f8ff}.overseas-stage-strip>div{display:flex;align-items:baseline;gap:10px}.overseas-stage-strip span,.overseas-stage-strip p{color:var(--cc-muted);font-size:12px}.overseas-stage-strip strong{color:var(--cc-primary);font-size:14px}.overseas-stage-strip p{margin:0}.overseas-stage-strip.is-overdue{background:#fff7ed}.overseas-stage-strip.is-overdue strong,.overseas-stage-strip.is-overdue p{color:#c2410c}
.workbench-tab-label { display:inline-flex; align-items:center; justify-content:center; gap:var(--admin-space-3); white-space:nowrap; }
.container-detail-page { display:grid; min-width:0; gap:var(--admin-space-5); }.container-overview { overflow:hidden; }.container-overview__header { display:grid; grid-template-columns:auto minmax(0,1fr) auto; align-items:center; gap:var(--admin-space-5); padding:var(--admin-space-4) var(--admin-space-5); }.container-overview__header h1 { margin:0; font-size:var(--admin-font-title); }.container-overview__header p { margin:2px 0 0; color:var(--cc-muted); font-size:var(--admin-font-caption); }.container-overview__summary { border-top:1px solid var(--cc-border); display:flex; flex-wrap:wrap; }.container-overview__summary>div { display:flex; min-width:0; padding:var(--admin-space-3) var(--admin-space-5); border-right:1px solid var(--cc-border); align-items:baseline; gap:var(--admin-space-2); }.container-overview__summary>div:last-child { border-right:0; }.container-overview__summary span,dt { color:var(--cc-muted); font-size:var(--admin-font-caption); }.container-overview__summary strong,dd { margin:0; white-space:normal; overflow-wrap:anywhere; }.detail-section{display:grid;min-width:0;gap:var(--admin-space-5);padding:var(--admin-space-5) var(--admin-space-5)}.detail-section>h2{margin:0;font-size:var(--admin-font-section)}.detail-fields { padding:0; }.outbound-panel{padding:var(--admin-space-5) 0 0;border-top:1px solid var(--cc-border)}.outbound-panel__header{display:flex;align-items:center;justify-content:space-between;gap:var(--admin-space-5)}.outbound-panel__header h2{margin:0;font-size:var(--admin-font-section)}.outbound-panel__header p,.outbound-panel__hint{margin:3px 0 0;color:var(--cc-muted);font-size:var(--admin-font-caption)}.outbound-panel__hint--success{color:var(--el-color-success)}.outbound-panel__actions{display:flex;align-items:center;gap:var(--admin-space-3)}.outbound-panel__loading{padding:var(--admin-space-3) 0}.gate-summary{display:flex;align-items:center;gap:var(--admin-space-5);margin-top:var(--admin-space-3);color:var(--cc-muted);font-size:var(--admin-font-caption)}.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:var(--admin-space-4);border-left:1px solid var(--cc-border)}.gate-list{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:var(--admin-space-2) var(--admin-space-5);margin-top:7px}.gate-row{display:grid;grid-template-columns:auto minmax(130px,.9fr) minmax(100px,1fr);align-items:center;gap:var(--admin-space-2) var(--admin-space-3);min-width:0;padding:var(--admin-space-2) var(--admin-space-3);border:1px solid var(--cc-border);border-radius:var(--admin-radius);background:var(--cc-surface-muted)}.gate-row__actual,.gate-row small{color:var(--cc-muted);font-size:var(--admin-font-caption);white-space:normal;overflow-wrap:anywhere}.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:var(--admin-font-caption)}.gate-row__coverage :deep(.el-tag){max-width:120px;overflow:hidden;text-overflow:ellipsis}.carrier-panel{padding-top:var(--admin-space-1)}@media(max-width:1200px){.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}}
.detail-section__heading { display:flex; align-items:center; justify-content:space-between; gap:var(--admin-space-5); }
.detail-section__heading h2 { margin:0; font-size:var(--admin-font-section); }
.overseas-stage-strip{display:flex;align-items:center;justify-content:space-between;gap:var(--admin-space-6);padding:9px var(--admin-space-5);border-top:1px solid var(--cc-border);background:#f5f8ff}.overseas-stage-strip>div{display:flex;align-items:baseline;gap:var(--admin-space-4)}.overseas-stage-strip span,.overseas-stage-strip p{color:var(--cc-muted);font-size:var(--admin-font-caption)}.overseas-stage-strip strong{color:var(--cc-primary);font-size:var(--admin-font-section)}.overseas-stage-strip p{margin:0}.overseas-stage-strip.is-overdue{background:#fff7ed}.overseas-stage-strip.is-overdue strong,.overseas-stage-strip.is-overdue p{color:#c2410c}.container-overview__header h1 { overflow-wrap:anywhere; }.workbench-tab-label { gap:var(--admin-space-2); }
</style>