优化
This commit is contained in:
@@ -36,7 +36,7 @@ describe('ContainerOverviewPanel', () => {
|
|||||||
expect(wrapper.text()).not.toContain('PHOTO_STEP_EMPTY_CONTAINER')
|
expect(wrapper.text()).not.toContain('PHOTO_STEP_EMPTY_CONTAINER')
|
||||||
expect(wrapper.text()).toContain('一项待复核')
|
expect(wrapper.text()).toContain('一项待复核')
|
||||||
expect(wrapper.text()).toContain('码头查验')
|
expect(wrapper.text()).toContain('码头查验')
|
||||||
expect(wrapper.text()).toContain('更新预计到港时间')
|
expect(wrapper.text()).not.toContain('更新预计到港时间')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('emits stable document types and does not expose file ids in visible text', async () => {
|
it('emits stable document types and does not expose file ids in visible text', async () => {
|
||||||
@@ -89,7 +89,7 @@ describe('ContainerOverviewPanel', () => {
|
|||||||
expect(section.text()).toContain('复核员')
|
expect(section.text()).toContain('复核员')
|
||||||
expect(section.text()).toContain('查验解除')
|
expect(section.text()).toContain('查验解除')
|
||||||
expect(section.text()).not.toContain('更新预计到港时间')
|
expect(section.text()).not.toContain('更新预计到港时间')
|
||||||
expect(wrapper.find('[aria-label="运输轨迹和操作日志"]').text()).toContain('更新预计到港时间')
|
expect(wrapper.find('[aria-label="运输轨迹和操作日志"]').exists()).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('emits the unchanged manage action for mark and cancel buttons', async () => {
|
it('emits the unchanged manage action for mark and cancel buttons', async () => {
|
||||||
@@ -137,7 +137,7 @@ describe('ContainerOverviewPanel', () => {
|
|||||||
wrapper.unmount()
|
wrapper.unmount()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('maps PDA events to Chinese and hides long technical keys from the main timeline', () => {
|
it('delegates warehouse events to the unified fulfillment timeline without exposing technical keys here', () => {
|
||||||
const longKey = 'pda-submit-1000001-2092438843630391297-2092437496092790785--0c642e80891d76a4-m9428-76'
|
const longKey = 'pda-submit-1000001-2092438843630391297-2092437496092790785--0c642e80891d76a4-m9428-76'
|
||||||
const timeline = [
|
const timeline = [
|
||||||
{ eventType: 'PDA_CLAIM', detail: `claim-${longKey}`, operatorId: '1', operatorName: 'platform_admin', occurredAt: '2026-08-26T10:28:00' },
|
{ eventType: 'PDA_CLAIM', detail: `claim-${longKey}`, operatorId: '1', operatorName: 'platform_admin', occurredAt: '2026-08-26T10:28:00' },
|
||||||
@@ -145,9 +145,9 @@ describe('ContainerOverviewPanel', () => {
|
|||||||
{ eventType: 'PDA_COMPLETE_LOADING', detail: `complete-${longKey}`, operatorId: '1', operatorName: 'platform_admin', occurredAt: '2026-08-26T10:29:00' },
|
{ eventType: 'PDA_COMPLETE_LOADING', detail: `complete-${longKey}`, operatorId: '1', operatorName: 'platform_admin', occurredAt: '2026-08-26T10:29:00' },
|
||||||
]
|
]
|
||||||
const wrapper = mount(ContainerOverviewPanel, { props: { overview: { ...overview, pda: null, timeline }, canManageDocuments: true, canManageCustoms: true, canManageException: true }, global: { plugins: [ElementPlus] } })
|
const wrapper = mount(ContainerOverviewPanel, { props: { overview: { ...overview, pda: null, timeline }, canManageDocuments: true, canManageCustoms: true, canManageException: true }, global: { plugins: [ElementPlus] } })
|
||||||
expect(wrapper.text()).toContain('领取 PDA 任务')
|
expect(wrapper.text()).not.toContain('领取 PDA 任务')
|
||||||
expect(wrapper.text()).toContain('提交拣货结果')
|
expect(wrapper.text()).not.toContain('提交拣货结果')
|
||||||
expect(wrapper.text()).toContain('完成装柜')
|
expect(wrapper.text()).not.toContain('完成装柜')
|
||||||
expect(wrapper.text()).not.toContain(longKey)
|
expect(wrapper.text()).not.toContain(longKey)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -89,13 +89,6 @@ function eventLabel(value: string) {
|
|||||||
PDA_CLAIM: '领取 PDA 任务', PDA_BATCH: '提交拣货结果', PDA_COMPLETE_LOADING: '完成装柜',
|
PDA_CLAIM: '领取 PDA 任务', PDA_BATCH: '提交拣货结果', PDA_COMPLETE_LOADING: '完成装柜',
|
||||||
} as Record<string, string>)[value] || value || '状态更新'
|
} as Record<string, string>)[value] || value || '状态更新'
|
||||||
}
|
}
|
||||||
function eventSummary(entry: ContainerOverview['timeline'][number]) {
|
|
||||||
if (entry.eventType === 'PDA_CLAIM') return 'PDA 作业任务已领取'
|
|
||||||
if (entry.eventType === 'PDA_BATCH') return '拣货结果已提交'
|
|
||||||
if (entry.eventType === 'PDA_COMPLETE_LOADING') return '装柜作业已完成'
|
|
||||||
const detail = String(entry.detail || '').trim()
|
|
||||||
return !detail || detail.length > 120 || /(?:operation|idempot|claim-|pda-submit|complete-)/i.test(detail) ? '操作已记录' : detail
|
|
||||||
}
|
|
||||||
function revokePhotoUrls() {
|
function revokePhotoUrls() {
|
||||||
Object.values(photoStates).forEach((state) => { if (state.url) URL.revokeObjectURL(state.url) })
|
Object.values(photoStates).forEach((state) => { if (state.url) URL.revokeObjectURL(state.url) })
|
||||||
Object.keys(photoStates).forEach((key) => delete photoStates[key])
|
Object.keys(photoStates).forEach((key) => delete photoStates[key])
|
||||||
@@ -153,17 +146,12 @@ onBeforeUnmount(() => { photoGeneration += 1; revokePhotoUrls() })
|
|||||||
<template v-else><div class="pda-summary"><el-progress :percentage="pdaProgress" :status="overview.pda.unresolvedItemCount ? 'exception' : undefined" /><div class="metric-grid"><div><span>任务明细</span><strong>{{ overview.pda.itemCount }}</strong></div><div><span>已处理</span><strong>{{ overview.pda.completedItemCount }}</strong></div><div><span>未解决</span><strong>{{ overview.pda.unresolvedItemCount }}</strong></div><div><span>有效照片</span><strong>{{ overview.pda.photoCounts.reduce((sum, item) => sum + item.count, 0) }}</strong></div></div></div><el-alert v-if="workflowStatusLabel(overview.pda.workflowStatus, overview.pda.taskStatus) === '待人工复核'" type="warning" :closable="false" title="PDA 作业已完成,等待人工复核后才能完成装柜。" /><el-alert v-else-if="workflowStatusLabel(overview.pda.workflowStatus, overview.pda.taskStatus) === '作业中'" type="info" :closable="false" title="请继续在 PDA 完成拣货、拍照和封条登记。" /><div v-if="activePhotos.length" class="photo-gallery"><button v-for="photo in activePhotos" :key="photo.evidenceId" type="button" class="photo-thumb" :aria-label="`预览装柜照片 ${photo.fileName}`" :disabled="!photoStates[photo.evidenceId]?.url" @click="openPhoto(photo)"><span v-if="photoStates[photo.evidenceId]?.loading" class="photo-placeholder">加载中</span><span v-else-if="photoStates[photo.evidenceId]?.error" class="photo-placeholder photo-placeholder--error">{{ photoStates[photo.evidenceId]?.error }}</span><img v-else-if="photoStates[photo.evidenceId]?.url" :src="photoStates[photo.evidenceId]?.url || ''" :alt="`${evidenceTypeLabel(photo.evidenceType)} ${photo.fileName}`" /><span class="photo-caption"><strong>{{ evidenceTypeLabel(photo.evidenceType) }}</strong><small>{{ formatTime(photo.uploadedAt) }}</small></span></button></div><p v-else class="compact-empty">暂无可查看的有效照片</p><el-alert v-if="overview.pda.exceptionReasons.length" type="warning" :closable="false" :title="overview.pda.exceptionReasons.join(';')" /></template>
|
<template v-else><div class="pda-summary"><el-progress :percentage="pdaProgress" :status="overview.pda.unresolvedItemCount ? 'exception' : undefined" /><div class="metric-grid"><div><span>任务明细</span><strong>{{ overview.pda.itemCount }}</strong></div><div><span>已处理</span><strong>{{ overview.pda.completedItemCount }}</strong></div><div><span>未解决</span><strong>{{ overview.pda.unresolvedItemCount }}</strong></div><div><span>有效照片</span><strong>{{ overview.pda.photoCounts.reduce((sum, item) => sum + item.count, 0) }}</strong></div></div></div><el-alert v-if="workflowStatusLabel(overview.pda.workflowStatus, overview.pda.taskStatus) === '待人工复核'" type="warning" :closable="false" title="PDA 作业已完成,等待人工复核后才能完成装柜。" /><el-alert v-else-if="workflowStatusLabel(overview.pda.workflowStatus, overview.pda.taskStatus) === '作业中'" type="info" :closable="false" title="请继续在 PDA 完成拣货、拍照和封条登记。" /><div v-if="activePhotos.length" class="photo-gallery"><button v-for="photo in activePhotos" :key="photo.evidenceId" type="button" class="photo-thumb" :aria-label="`预览装柜照片 ${photo.fileName}`" :disabled="!photoStates[photo.evidenceId]?.url" @click="openPhoto(photo)"><span v-if="photoStates[photo.evidenceId]?.loading" class="photo-placeholder">加载中</span><span v-else-if="photoStates[photo.evidenceId]?.error" class="photo-placeholder photo-placeholder--error">{{ photoStates[photo.evidenceId]?.error }}</span><img v-else-if="photoStates[photo.evidenceId]?.url" :src="photoStates[photo.evidenceId]?.url || ''" :alt="`${evidenceTypeLabel(photo.evidenceType)} ${photo.fileName}`" /><span class="photo-caption"><strong>{{ evidenceTypeLabel(photo.evidenceType) }}</strong><small>{{ formatTime(photo.uploadedAt) }}</small></span></button></div><p v-else class="compact-empty">暂无可查看的有效照片</p><el-alert v-if="overview.pda.exceptionReasons.length" type="warning" :closable="false" :title="overview.pda.exceptionReasons.join(';')" /></template>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="overview-section" aria-label="运输轨迹和操作日志">
|
|
||||||
<header><h3>运输轨迹与操作日志</h3><span>{{ overview.timeline.length }} 条</span></header>
|
|
||||||
<el-empty v-if="overview.timeline.length === 0" description="暂无轨迹或操作记录" :image-size="64" />
|
|
||||||
<ol v-else class="compact-timeline"><li v-for="(entry, index) in overview.timeline" :key="`${entry.eventType}-${entry.occurredAt}-${index}`"><time>{{ formatTime(entry.occurredAt) }}</time><strong>{{ eventLabel(entry.eventType) }}</strong><span>{{ entry.operatorName || '系统' }}</span><small>{{ eventSummary(entry) }}</small></li></ol>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.overview-panel { display: grid; gap: 12px; }.overview-section:nth-child(3){order:1}.overview-section:nth-child(2){order:2}.overview-section:nth-child(1){order:3}.overview-section:nth-child(4){order:4}
|
.overview-panel { display: grid; gap: 12px; }.overview-section:nth-child(3){order:1}.overview-section:nth-child(2){order:2}.overview-section:nth-child(1){order:3}
|
||||||
.overview-section { min-width: 0; padding: 12px 16px; border: 1px solid var(--cc-border); background: #fff; }
|
.overview-section { min-width: 0; padding: 12px 16px; border: 1px solid var(--cc-border); background: #fff; }
|
||||||
.overview-section > header { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 14px; }
|
.overview-section > header { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 14px; }
|
||||||
.overview-section h3 { margin: 0; font-size: 15px; }
|
.overview-section h3 { margin: 0; font-size: 15px; }
|
||||||
|
|||||||
@@ -372,7 +372,7 @@ onMounted(async () => { await load(); if (route.query.action === 'documents') op
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</section>
|
</section>
|
||||||
<div class="surface-panel carrier-panel"><CarrierScheduleQueryPanel :key="carrierRefreshKey" :ocean-booking-id="detail.oceanBookingId" :container-booking-id="detail.id" /></div>
|
<div class="surface-panel carrier-panel"><CarrierScheduleQueryPanel :key="carrierRefreshKey" :ocean-booking-id="detail.oceanBookingId" :container-booking-id="detail.id" :warehouse-timeline="overview?.timeline || []" /></div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<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 :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="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 :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>
|
||||||
|
|||||||
@@ -51,10 +51,14 @@ describe('CarrierScheduleQueryPanel loading states', () => {
|
|||||||
|
|
||||||
it('shows an explicit non-fabricated empty state and performs no detail reads', async () => {
|
it('shows an explicit non-fabricated empty state and performs no detail reads', async () => {
|
||||||
vi.mocked(listCarrierScheduleQueries).mockResolvedValue({ records: [], page: 1, size: 200, total: 0, pages: 0 })
|
vi.mocked(listCarrierScheduleQueries).mockResolvedValue({ records: [], page: 1, size: 200, total: 0, pages: 0 })
|
||||||
const wrapper = mount(CarrierScheduleQueryPanel, { props: { oceanBookingId: '2' }, global: { plugins: [ElementPlus, pinia] } })
|
const wrapper = mount(CarrierScheduleQueryPanel, { props: { oceanBookingId: '2', warehouseTimeline: [{ eventType: 'PDA_COMPLETE_LOADING', detail: 'complete-key-technical', operatorName: '仓库员', occurredAt: '2026-08-20T08:00:00' }] }, global: { plugins: [ElementPlus, pinia] } })
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
|
|
||||||
expect(wrapper.text()).toContain('本页面不会创建或模拟任务')
|
expect(wrapper.text()).toContain('货柜履约轨迹')
|
||||||
|
expect(wrapper.text()).toContain('完成装柜')
|
||||||
|
expect(wrapper.text()).toContain('仓内操作')
|
||||||
|
expect(wrapper.text()).toContain('确认离仓后自动开始船节点跟踪')
|
||||||
|
expect(wrapper.findAll('.tracking-prompt')).toHaveLength(1)
|
||||||
expect(getCarrierScheduleQuery).not.toHaveBeenCalled()
|
expect(getCarrierScheduleQuery).not.toHaveBeenCalled()
|
||||||
expect(getPortSchedule).not.toHaveBeenCalled()
|
expect(getPortSchedule).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
@@ -103,9 +107,39 @@ describe('CarrierScheduleQueryPanel loading states', () => {
|
|||||||
expect(wrapper.text()).toContain('最新实际节点已离开国内仓库')
|
expect(wrapper.text()).toContain('最新实际节点已离开国内仓库')
|
||||||
expect(wrapper.text()).toContain('预计 2026-08-28 12:00')
|
expect(wrapper.text()).toContain('预计 2026-08-28 12:00')
|
||||||
expect(wrapper.text()).not.toContain('最新实际节点船舶已到目的港')
|
expect(wrapper.text()).not.toContain('最新实际节点船舶已到目的港')
|
||||||
expect(wrapper.text()).toContain('异常事实')
|
expect(wrapper.text()).toContain('异常')
|
||||||
expect(wrapper.text()).toContain('海关扣留 · 发生')
|
expect(wrapper.text()).toContain('海关扣留 · 发生')
|
||||||
expect(wrapper.text()).toContain('Customs hold')
|
expect(wrapper.text()).toContain('Customs hold')
|
||||||
|
expect(wrapper.text()).toContain('船运跟踪 · HEDE · hede-hold')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('merges warehouse, actual, estimated, manual and exception facts with stable ordering and deduplication', async () => {
|
||||||
|
vi.mocked(listCarrierScheduleQueries).mockResolvedValue({ records: [{ ...queryView, query: { ...queryView.query, resultPortScheduleId: null }, expired: false }], page: 1, size: 200, total: 1, pages: 1 })
|
||||||
|
const actual = { id: '61', providerCode: 'COSCO', providerEventId: 'actual-61', providerNodeCode: 'LOAD', standardNode: 'LOADED_ON_VESSEL' as const, nodeSequence: 30, observedAt: '2026-08-21T10:05:00', estimatedAt: null, actualAt: '2026-08-21T10:00:00', locationCode: 'CNNGB', locationName: '宁波港', sourceReference: 'actual-source', rawPayloadHash: 'e'.repeat(64), createdAt: '2026-08-21T10:06:00' }
|
||||||
|
const estimated = { id: '62', providerCode: 'COSCO', providerEventId: 'estimate-62', providerNodeCode: 'ETA', standardNode: 'ARRIVED_DESTINATION_PORT' as const, nodeSequence: 60, observedAt: '2026-08-20T10:00:00', estimatedAt: '2026-08-21T10:00:00', actualAt: null, locationCode: 'USLAX', locationName: 'Los Angeles', sourceReference: 'estimate-source', rawPayloadHash: 'f'.repeat(64), createdAt: '2026-08-20T10:01:00' }
|
||||||
|
const manual = { id: '63', providerCode: 'MANUAL', providerEventId: 'manual-63', providerNodeCode: 'TRANSIT', standardNode: 'IN_TRANSIT' as const, nodeSequence: 50, observedAt: '2026-08-21T09:00:00', estimatedAt: null, actualAt: '2026-08-21T09:00:00', locationCode: null, locationName: '太平洋', sourceReference: '人工核验', rawPayloadHash: 'g'.repeat(64), createdAt: '2026-08-21T09:01:00' }
|
||||||
|
vi.mocked(getCarrierScheduleQuery).mockResolvedValue({ query: { ...queryView, query: { ...queryView.query, resultPortScheduleId: null }, expired: false }, nodes: [estimated, actual, { ...actual, id: '64' }, manual], exceptions: [{ id: '65', providerCode: 'COSCO', providerEventId: 'hold-65', providerStatus: 'Terminal hold', exceptionCode: 'TERMINAL_HOLD', active: true, observedAt: '2026-08-21T08:30:00', locationCode: 'USLAX', locationName: 'Los Angeles', sourceReference: 'hold-source', rawPayloadHash: 'h'.repeat(64), createdAt: '2026-08-21T08:31:00' }], histories: [{ id: '66', action: 'MANUAL_NODE', fromVersion: 1, toVersion: 2, reason: '人工核验', operatorId: '1', operatorName: '管理员', createdAt: '2026-08-21T09:01:00' }] })
|
||||||
|
const wrapper = mount(CarrierScheduleQueryPanel, { props: { oceanBookingId: '2', warehouseTimeline: [{ eventType: 'PDA_COMPLETE_LOADING', detail: '装柜完成', operatorName: '仓库员', occurredAt: '2026-08-20T08:00:00' }] }, global: { plugins: [ElementPlus, pinia] } })
|
||||||
|
await flushPromises()
|
||||||
|
const rows = wrapper.findAll('.fulfillment-timeline li')
|
||||||
|
expect(rows.filter((row) => row.text().includes('已装船'))).toHaveLength(1)
|
||||||
|
expect(rows[0]?.text()).toContain('已装船')
|
||||||
|
expect(rows[1]?.text()).toContain('预计 2026-08-21 10:00')
|
||||||
|
expect(rows[1]?.text()).toContain('船运节点')
|
||||||
|
expect(rows.map((row) => row.text()).join('|')).toContain('人工补录')
|
||||||
|
expect(rows.map((row) => row.text()).join('|')).toContain('码头扣留 · 发生')
|
||||||
|
expect(wrapper.text()).toContain('查询任务摘要、异常与变更历史')
|
||||||
|
expect(wrapper.text()).toContain('人工补录标准节点')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('hides the manual action without the frozen permission while retaining fulfillment facts', async () => {
|
||||||
|
useAuthStore().permissions = []
|
||||||
|
vi.mocked(listCarrierScheduleQueries).mockResolvedValue({ records: [{ ...queryView, query: { ...queryView.query, resultPortScheduleId: null }, expired: false }], page: 1, size: 200, total: 1, pages: 1 })
|
||||||
|
vi.mocked(getCarrierScheduleQuery).mockResolvedValue({ query: { ...queryView, query: { ...queryView.query, resultPortScheduleId: null }, expired: false }, nodes: [], exceptions: [], histories: [] })
|
||||||
|
const wrapper = mount(CarrierScheduleQueryPanel, { props: { oceanBookingId: '2', warehouseTimeline: [{ eventType: 'PDA_BATCH', detail: '拣货结果已提交', operatorName: '仓库员', occurredAt: '2026-08-20T07:00:00' }] }, global: { plugins: [ElementPlus, pinia] } })
|
||||||
|
await flushPromises()
|
||||||
|
expect(wrapper.text()).toContain('提交拣货结果')
|
||||||
|
expect(wrapper.findAll('button').some((button) => button.text().includes('人工补录节点'))).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('keeps the form context and dialog-scoped idempotency key when a conflict is retried', async () => {
|
it('keeps the form context and dialog-scoped idempotency key when a conflict is retried', async () => {
|
||||||
|
|||||||
@@ -16,15 +16,13 @@ describe('CarrierScheduleQueryPanel', () => {
|
|||||||
|
|
||||||
it('shows source, observation/update times, history, failures and expiry truthfully', () => {
|
it('shows source, observation/update times, history, failures and expiry truthfully', () => {
|
||||||
expect(source).toContain('最新来源')
|
expect(source).toContain('最新来源')
|
||||||
expect(source).toContain('访问方式')
|
|
||||||
expect(source).toContain('观测时间')
|
expect(source).toContain('观测时间')
|
||||||
expect(source).toContain('来源更新时间')
|
expect(source).toContain('来源参考')
|
||||||
expect(source).toContain('标准节点历史')
|
expect(source).toContain('providerCode')
|
||||||
expect(source).toContain('节点位置')
|
expect(source).toContain('sourceReference')
|
||||||
expect(source).toContain('任务操作状态')
|
|
||||||
expect(source).toContain('实际发生时间')
|
expect(source).toContain('实际发生时间')
|
||||||
expect(source).toContain('变更历史')
|
expect(source).toContain('变更历史')
|
||||||
expect(source).toContain('query.lastErrorMessage')
|
expect(source).toContain('query?.lastErrorMessage')
|
||||||
expect(source).toContain('query.lastErrorCode')
|
expect(source).toContain('query.lastErrorCode')
|
||||||
expect(source).toContain("queryView?.expired ? '结果已过期' : '结果有效'")
|
expect(source).toContain("queryView?.expired ? '结果已过期' : '结果有效'")
|
||||||
})
|
})
|
||||||
@@ -41,20 +39,20 @@ describe('CarrierScheduleQueryPanel', () => {
|
|||||||
expect(source).not.toMatch(new RegExp(`\\b${['ARR', 'IVED'].join('')}:`))
|
expect(source).not.toMatch(new RegExp(`\\b${['ARR', 'IVED'].join('')}:`))
|
||||||
expect(source).toContain("latestActualNode === 'EMPTY_RETURNED'")
|
expect(source).toContain("latestActualNode === 'EMPTY_RETURNED'")
|
||||||
expect(source).toContain("'已停止 · 已还柜'")
|
expect(source).toContain("'已停止 · 已还柜'")
|
||||||
expect(source).toContain('任务仅在确认国内出仓后自动建立')
|
expect(source).toContain('确认离仓后自动开始船节点跟踪')
|
||||||
expect(source).not.toContain('/' + 'scheduler')
|
expect(source).not.toContain('/' + 'scheduler')
|
||||||
expect(source).not.toContain('/' + 'attempts')
|
expect(source).not.toContain('/' + 'attempts')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('uses a compact no-task state without the large empty illustration', () => {
|
it('uses a compact no-task state without the large empty illustration', () => {
|
||||||
expect(source).toContain('class="compact-empty-state"')
|
expect(source).toContain('class="tracking-prompt"')
|
||||||
expect(source).not.toContain('<el-empty v-else-if="!query"')
|
expect(source).not.toContain('<el-empty v-else-if="!query"')
|
||||||
expect(source).toContain('任务仅在确认国内出仓后自动建立')
|
expect(source).toContain('确认离仓后自动开始船节点跟踪')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('keeps estimated facts labelled and separate from actual progress', () => {
|
it('keeps estimated facts labelled and separate from actual progress', () => {
|
||||||
expect(source).toContain("item.actualAt ? `实际")
|
expect(source).toContain("actual ? `实际")
|
||||||
expect(source).toContain(": item.estimatedAt ? `预计")
|
expect(source).toContain("item.estimatedAt ? `预计")
|
||||||
expect(source).toContain('latestActualNode')
|
expect(source).toContain('latestActualNode')
|
||||||
expect(source).toContain('item.actualAt')
|
expect(source).toContain('item.actualAt')
|
||||||
})
|
})
|
||||||
@@ -63,7 +61,8 @@ describe('CarrierScheduleQueryPanel', () => {
|
|||||||
expect(source).toContain("access.can('platform:firstleg:port-schedule-query:manual')")
|
expect(source).toContain("access.can('platform:firstleg:port-schedule-query:manual')")
|
||||||
expect(source).toContain('appendCarrierScheduleManualNode')
|
expect(source).toContain('appendCarrierScheduleManualNode')
|
||||||
expect(source).toContain('manualIdempotencyKey')
|
expect(source).toContain('manualIdempotencyKey')
|
||||||
expect(source).toContain('异常事实')
|
expect(source).toContain("type: '异常'")
|
||||||
|
expect(source).toContain('船运跟踪')
|
||||||
expect(source).toContain('item.providerStatus')
|
expect(source).toContain('item.providerStatus')
|
||||||
expect(source).not.toContain('/' + 'manual-results')
|
expect(source).not.toContain('/' + 'manual-results')
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -10,7 +10,9 @@ 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'
|
||||||
|
|
||||||
const props = defineProps<{ oceanBookingId?: string | null; containerBookingId?: string | null; portScheduleId?: string | null }>()
|
type WarehouseTimelineEntry = { eventType: string; detail?: string | null; operatorName?: string | null; occurredAt?: string | null }
|
||||||
|
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 access = useAccessPreviewStore()
|
const access = useAccessPreviewStore()
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const error = ref<ApiErrorDetails | null>(null)
|
const error = ref<ApiErrorDetails | null>(null)
|
||||||
@@ -60,6 +62,40 @@ 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 warehouseEventSummary(entry: WarehouseTimelineEntry) {
|
||||||
|
const value = String(entry.detail || '').trim()
|
||||||
|
return !value || value.length > 120 || /(?:operation|idempot|claim-|pda-submit|complete-)/i.test(value) ? '操作已记录' : value
|
||||||
|
}
|
||||||
|
const fulfillmentEvents = computed<FulfillmentEvent[]>(() => {
|
||||||
|
const values: FulfillmentEvent[] = []
|
||||||
|
for (const [index, item] of (props.warehouseTimeline || []).entries()) {
|
||||||
|
if (!item.occurredAt || ['MARK', 'CANCEL'].includes(item.eventType)) continue
|
||||||
|
const departure = /OUTBOUND|DISPATCH|DEPARTED_WAREHOUSE/.test(item.eventType)
|
||||||
|
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 })
|
||||||
|
}
|
||||||
|
const nodeKeys = new Set<string>()
|
||||||
|
for (const item of nodes.value) {
|
||||||
|
const actual = Boolean(item.actualAt)
|
||||||
|
const time = item.actualAt || item.estimatedAt || item.observedAt
|
||||||
|
const key = JSON.stringify([item.standardNode, item.actualAt || null, item.estimatedAt || null, item.locationCode || null, item.locationName || null, item.providerCode, item.sourceReference || null])
|
||||||
|
if (nodeKeys.has(key)) continue
|
||||||
|
nodeKeys.add(key)
|
||||||
|
const manual = /MANUAL|PLATFORM/i.test(item.providerCode)
|
||||||
|
const timing = actual ? `实际 ${formatTime(item.actualAt)}` : item.estimatedAt ? `预计 ${formatTime(item.estimatedAt)}` : `观测 ${formatTime(item.observedAt)}`
|
||||||
|
const expected = actual && item.estimatedAt ? ` · 原预计 ${formatTime(item.estimatedAt)}` : ''
|
||||||
|
const type = manual ? '人工补录' : item.standardNode === 'DEPARTED_WAREHOUSE' && actual ? '确认离仓' : '船运节点'
|
||||||
|
values.push({ key: `node-${key}`, time, type, title: nodeText(item.standardNode), summary: `${timing}${expected} · ${nodeLocation(item)}`, source: [item.providerCode, item.sourceReference].filter(Boolean).join(' · ') || '—', actual })
|
||||||
|
}
|
||||||
|
for (const item of exceptions.value) values.push({ key: `exception-${item.id}`, time: item.observedAt, type: '异常', title: `${exceptionText(item.exceptionCode)} · ${item.active ? '发生' : '已解除'}`, summary: `${exceptionLocation(item)} · ${item.providerStatus}`, source: `船运跟踪 · ${[item.providerCode, item.sourceReference].filter(Boolean).join(' · ')}`, actual: true })
|
||||||
|
return values.sort((left, right) => {
|
||||||
|
const time = Date.parse(right.time) - Date.parse(left.time)
|
||||||
|
if (time) return time
|
||||||
|
const actual = Number(right.actual) - Number(left.actual)
|
||||||
|
return actual || left.key.localeCompare(right.key)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
function eventTagType(type: FulfillmentEvent['type']) { return ({ '仓内操作': 'info', '确认离仓': 'success', '船运节点': 'primary', '人工补录': 'warning', '异常': 'danger' } as const)[type] }
|
||||||
|
|
||||||
function resetManualForm() {
|
function resetManualForm() {
|
||||||
Object.assign(manualForm, { standardNode: '', estimatedAt: '', actualAt: '', locationCode: '', locationName: '', reason: '' })
|
Object.assign(manualForm, { standardNode: '', estimatedAt: '', actualAt: '', locationCode: '', locationName: '', reason: '' })
|
||||||
@@ -114,33 +150,20 @@ watch(manualDialogOpen, (open) => { if (!open && !manualSaving.value) { manualId
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="carrier-query" aria-label="国内出仓后船节点">
|
<section class="carrier-query" aria-label="货柜履约轨迹">
|
||||||
<header><div><h2>国内出仓后船节点</h2><p>沿用 CarrierScheduleQuery,从确认国内出仓跟踪至已还柜;本页面不会新建轮询任务。</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"><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="2" animated /></div>
|
<div v-else-if="loading" class="loading-state"><el-skeleton :rows="1" animated /></div>
|
||||||
<div v-else-if="!query" class="compact-empty-state"><strong>尚无关联任务</strong><span>任务仅在确认国内出仓后自动建立,本页面不会创建或模拟任务。</span></div>
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<el-alert v-if="query.lastErrorMessage || query.status === 'MANUAL_REQUIRED'" type="error" :closable="false" show-icon :title="query.lastErrorMessage || query.manualReason || '自动查询暂不可用,需要人工补充可靠结果。'" />
|
<el-alert v-if="query?.lastErrorMessage || query?.status === 'MANUAL_REQUIRED'" type="error" :closable="false" show-icon :title="query?.lastErrorMessage || query?.manualReason || '自动查询暂不可用,需要人工补充可靠结果。'" />
|
||||||
<el-alert v-if="query.status === 'STOPPED'" type="info" :closable="false" show-icon :title="query.stopReason || '船节点查询已停止。'" />
|
<el-alert v-if="query?.status === 'STOPPED'" type="info" :closable="false" show-icon :title="query.stopReason || '船节点查询已停止。'" />
|
||||||
<el-alert v-if="queryDetailError" type="warning" :closable="false" show-icon title="标准节点与变更历史暂不可用"><template #default><ApiErrorMeta :details="queryDetailError" /></template></el-alert>
|
<el-alert v-if="queryDetailError" type="warning" :closable="false" show-icon title="标准节点与变更历史暂不可用"><template #default><ApiErrorMeta :details="queryDetailError" /></template></el-alert>
|
||||||
<el-alert v-if="scheduleError" type="warning" :closable="false" show-icon title="关联船期结果暂不可用"><template #default><ApiErrorMeta :details="scheduleError" /></template></el-alert>
|
<el-alert v-if="scheduleError" type="warning" :closable="false" show-icon title="关联船期结果暂不可用"><template #default><ApiErrorMeta :details="scheduleError" /></template></el-alert>
|
||||||
<dl class="query-facts">
|
|
||||||
<div><dt>查询任务</dt><dd>{{ query.queryNo || query.id }}</dd></div><div><dt>跟踪标识</dt><dd>{{ trackingTypeText(query.trackingType) }} · {{ query.trackingNo || '—' }}</dd></div>
|
|
||||||
<div><dt>任务操作状态</dt><dd>{{ statusText }}</dd></div><div><dt>建立来源</dt><dd>{{ triggerSourceText(query.triggerSource) }}</dd></div>
|
|
||||||
<div><dt>最新实际节点</dt><dd>{{ nodeText(query.latestActualNode) }}</dd></div><div><dt>实际发生时间</dt><dd>{{ formatTime(query.latestActualAt) }}</dd></div>
|
|
||||||
<div><dt>节点位置</dt><dd>{{ nodeLocation(latestActualNodeFact) }}</dd></div><div><dt>观测时间</dt><dd>{{ formatTime(latestObservation) }}</dd></div>
|
|
||||||
<div><dt>船司 / 访问方式</dt><dd>{{ query.carrierCode }} · {{ query.accessMode || '—' }}</dd></div><div><dt>最新来源</dt><dd>{{ latestSource }}</dd></div>
|
|
||||||
<div><dt>来源参考</dt><dd class="break-value">{{ sourceReference }}</dd></div><div><dt>来源更新时间</dt><dd>{{ formatTime(query.sourceUpdatedAt) }}</dd></div>
|
|
||||||
<div><dt>最近成功</dt><dd>{{ formatTime(query.lastSuccessAt) }}</dd></div><div><dt>结果有效性</dt><dd>{{ queryView?.expired ? '结果已过期' : '结果有效' }}</dd></div>
|
|
||||||
<div><dt>结果过期时间</dt><dd>{{ formatTime(query.expiresAt) }}</dd></div><div><dt>停止时间</dt><dd>{{ formatTime(query.stoppedAt) }}</dd></div>
|
|
||||||
<div v-if="query.lastErrorCode"><dt>最近错误码</dt><dd>{{ query.lastErrorCode }}</dd></div>
|
|
||||||
</dl>
|
|
||||||
<div v-if="latestSchedule" class="latest-result"><strong>{{ latestSchedule.schedule.vesselName }} / {{ latestSchedule.schedule.voyageNo }}</strong><span>{{ latestSchedule.schedule.departurePortName || latestSchedule.schedule.departurePortCode }} → {{ latestSchedule.schedule.destinationPortName || latestSchedule.schedule.destinationPortCode }}</span><span>预计开船 {{ formatTime(latestSchedule.schedule.etdAt) }} · 预计到港 {{ formatTime(latestSchedule.schedule.etaAt) }}</span></div>
|
<div v-if="latestSchedule" class="latest-result"><strong>{{ latestSchedule.schedule.vesselName }} / {{ latestSchedule.schedule.voyageNo }}</strong><span>{{ latestSchedule.schedule.departurePortName || latestSchedule.schedule.departurePortCode }} → {{ latestSchedule.schedule.destinationPortName || latestSchedule.schedule.destinationPortCode }}</span><span>预计开船 {{ formatTime(latestSchedule.schedule.etdAt) }} · 预计到港 {{ formatTime(latestSchedule.schedule.etaAt) }}</span></div>
|
||||||
<div class="history-grid">
|
<ol v-if="fulfillmentEvents.length" class="fulfillment-timeline"><li v-for="item in fulfillmentEvents" :key="item.key"><time>{{ formatTime(item.time) }}</time><el-tag size="small" :type="eventTagType(item.type)">{{ item.type }}</el-tag><strong>{{ item.title }}</strong><span>{{ item.summary }}</span><small>{{ item.source }}</small></li></ol>
|
||||||
<section><h3>标准节点历史</h3><p v-if="!nodes.length" class="empty-line">暂无标准节点</p><ol v-else><li v-for="item in nodes" :key="item.id"><span>{{ nodeTimeText(item) }}</span><strong>{{ nodeText(item.standardNode) }}</strong><small>{{ nodeLocation(item) }} · {{ item.providerCode }}<template v-if="item.sourceReference"> · {{ item.sourceReference }}</template><template v-if="item.actualAt && item.estimatedAt"> · 预计 {{ formatTime(item.estimatedAt) }}</template></small></li></ol></section>
|
<p v-else class="empty-line">暂无货柜履约记录</p>
|
||||||
<section><h3>异常事实</h3><p v-if="!exceptions.length" class="empty-line">暂无异常事实</p><ol v-else><li v-for="item in exceptions" :key="item.id"><span>{{ formatTime(item.observedAt) }}</span><strong>{{ exceptionText(item.exceptionCode) }} · {{ item.active ? '发生' : '已解除' }}</strong><small>{{ exceptionLocation(item) }} · {{ item.providerCode }} · {{ item.providerStatus }}<template v-if="item.sourceReference"> · {{ item.sourceReference }}</template></small></li></ol></section>
|
<p v-if="!query" class="tracking-prompt">确认离仓后自动开始船节点跟踪</p>
|
||||||
<section><h3>变更历史</h3><p v-if="!histories.length" class="empty-line">暂无变更历史</p><ol v-else><li v-for="item in histories" :key="item.id"><span>{{ formatTime(item.createdAt) }}</span><strong>{{ historyText(item.action) }}</strong><small>{{ item.operatorName || '系统' }}<template v-if="item.reason"> · {{ item.reason }}</template></small></li></ol></section>
|
<details v-else class="technical-details"><summary>查询任务摘要、异常与变更历史</summary><dl class="query-facts"><div><dt>查询任务</dt><dd>{{ query.queryNo || query.id }}</dd></div><div><dt>跟踪标识</dt><dd>{{ trackingTypeText(query.trackingType) }} · {{ query.trackingNo || '—' }}</dd></div><div><dt>任务状态</dt><dd>{{ statusText }}</dd></div><div><dt>建立来源</dt><dd>{{ triggerSourceText(query.triggerSource) }}</dd></div><div><dt>最新实际节点</dt><dd>{{ nodeText(query.latestActualNode) }}</dd></div><div><dt>实际发生时间</dt><dd>{{ formatTime(query.latestActualAt) }}</dd></div><div><dt>最新来源</dt><dd>{{ latestSource }}</dd></div><div><dt>来源参考</dt><dd class="break-value">{{ sourceReference }}</dd></div><div><dt>观测时间</dt><dd>{{ formatTime(latestObservation) }}</dd></div><div><dt>最近成功</dt><dd>{{ formatTime(query.lastSuccessAt) }}</dd></div><div><dt>结果有效性</dt><dd>{{ queryView?.expired ? '结果已过期' : '结果有效' }}</dd></div><div v-if="query.lastErrorCode"><dt>最近错误码</dt><dd>{{ query.lastErrorCode }}</dd></div></dl><h3>任务变更历史</h3><p v-if="!histories.length" class="empty-line">暂无变更历史</p><ol v-else class="technical-history"><li v-for="item in histories" :key="item.id"><span>{{ formatTime(item.createdAt) }}</span><strong>{{ historyText(item.action) }}</strong><small>{{ item.operatorName || '系统' }}<template v-if="item.reason"> · {{ item.reason }}</template></small></li></ol></details>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
<AppDialog v-model="manualDialogOpen" title="人工补录船节点" width="min(620px, calc(100vw - 28px))" confirm-text="确认补录" :confirm-loading="manualSaving" @confirm="submitManualNode">
|
<AppDialog v-model="manualDialogOpen" title="人工补录船节点" width="min(620px, calc(100vw - 28px))" confirm-text="确认补录" :confirm-loading="manualSaving" @confirm="submitManualNode">
|
||||||
<el-alert v-if="manualError" type="error" :closable="false" show-icon title="补录未成功,已保留当前填写内容"><template #default><ApiErrorMeta :details="manualError" /></template></el-alert>
|
<el-alert v-if="manualError" type="error" :closable="false" show-icon title="补录未成功,已保留当前填写内容"><template #default><ApiErrorMeta :details="manualError" /></template></el-alert>
|
||||||
@@ -157,5 +180,5 @@ watch(manualDialogOpen, (open) => { if (!open && !manualSaving.value) { manualId
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.carrier-query{display:grid;gap:12px;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:4px 0 0;color:var(--cc-muted);font-size:12px}.loading-state{padding:8px 0}.compact-empty-state{display:flex;align-items:center;gap:10px;padding:9px 12px;border-left:3px solid var(--el-color-info);background:var(--cc-bg-subtle,#f7f8fa);font-size:12px}.compact-empty-state span{color:var(--cc-muted)}.query-facts{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px 18px;margin:0}.query-facts>div{min-width:0}.query-facts dt{color:var(--cc-muted);font-size:12px}.query-facts dd{margin:4px 0 0;font-size:13px}.break-value{overflow-wrap:anywhere}.latest-result{display:flex;flex-wrap:wrap;align-items:center;gap:8px 18px;padding:10px 12px;border-left:3px solid var(--el-color-primary);background:var(--el-color-primary-light-9);font-size:13px}.history-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px}.history-grid section{min-width:0}.history-grid h3{margin:0 0 8px;font-size:13px}.history-grid ol{display:grid;gap:0;margin:0;padding:0;list-style:none}.history-grid li{display:grid;grid-template-columns:128px 120px minmax(0,1fr);gap:8px;padding:7px 0;border-top:1px solid var(--cc-border);font-size:12px}.history-grid small,.empty-line{color:var(--cc-muted);font-size:12px}.empty-line{margin:0}.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))}}@media(max-width:720px){.query-facts,.history-grid,.manual-form-grid{grid-template-columns:1fr}.history-grid li{grid-template-columns:1fr}.compact-empty-state{align-items:flex-start;flex-direction:column}}
|
.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>
|
||||||
|
|||||||
Reference in New Issue
Block a user