This commit is contained in:
2026-08-19 16:45:51 +08:00
parent d1de19dc82
commit 65cd9ecefa
79 changed files with 3397 additions and 38 deletions
@@ -0,0 +1,42 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { http } from '@/shared/api/http'
import { getCarrierScheduleQuery, listCarrierScheduleQueries } from './platformCarrierScheduleQueries'
vi.mock('@/shared/api/http', async () => {
const actual = await vi.importActual<typeof import('@/shared/api/http')>('@/shared/api/http')
return { ...actual, http: { get: vi.fn() } }
})
const query = {
id: 9007199254740993n,
queryNo: 'CSQ-1',
customerId: 101,
oceanBookingId: 202,
containerBookingId: 303,
carrierCode: 'COSCO',
trackingType: 'CONTAINER_NO',
trackingNo: 'CSNU1234567',
status: 'MANUAL_REQUIRED',
attemptCount: '2',
resultPortScheduleId: 404,
version: '3',
}
describe('platformCarrierScheduleQueries', () => {
beforeEach(() => vi.clearAllMocks())
it('filters by real booking/container relations and preserves long ids as strings', async () => {
vi.mocked(http.get).mockResolvedValue({ code: '200', data: { records: [{ query, expired: true, maxAttempts: '3', nextAllowedAt: null }], page: '1', size: '200', total: '1', pages: '1' } })
const result = await listCarrierScheduleQueries({ page: 1, size: 200, oceanBookingId: '202', containerBookingId: '303' })
expect(vi.mocked(http.get)).toHaveBeenCalledWith('/platform/v1/carrier-schedule-queries', { params: { page: 1, size: 200, oceanBookingId: '202', containerBookingId: '303' } })
expect(result.records[0]).toMatchObject({ query: { id: '9007199254740993', customerId: '101', oceanBookingId: '202', containerBookingId: '303', resultPortScheduleId: '404', attemptCount: 2, version: 3 }, expired: true, maxAttempts: 3 })
})
it('normalizes attempts and immutable history without creating query operations', async () => {
vi.mocked(http.get).mockResolvedValue({ code: '200', data: { query: { query, expired: false, maxAttempts: 3 }, attempts: [{ id: 1, queryId: query.id, windowAttemptNo: '2', carrierCode: 'COSCO', attemptStatus: 'FAILED', coverageGap: true }], histories: [{ id: 2, queryId: query.id, action: 'ATTEMPT', fromVersion: '1', toVersion: '2' }] } })
const result = await getCarrierScheduleQuery('9007199254740993')
expect(vi.mocked(http.get)).toHaveBeenCalledWith('/platform/v1/carrier-schedule-queries/9007199254740993')
expect(result.attempts[0]).toMatchObject({ id: '1', queryId: '9007199254740993', windowAttemptNo: 2, attemptStatus: 'FAILED' })
expect(result.histories[0]).toMatchObject({ id: '2', fromVersion: 1, toVersion: 2 })
})
})
@@ -0,0 +1,52 @@
import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
import type {
CarrierScheduleQueryAttempt,
CarrierScheduleQueryDetail,
CarrierScheduleQueryFilter,
CarrierScheduleQueryHistory,
CarrierScheduleQueryPage,
CarrierScheduleQueryResource,
CarrierScheduleQueryView,
} from '../types'
const basePath = '/platform/v1/carrier-schedule-queries'
function optionalId(value: unknown): string | null { return value == null ? null : String(value) }
function numberValue(value: unknown, fallback = 0): number { const parsed = Number(value); return Number.isFinite(parsed) ? parsed : fallback }
function resource(raw: CarrierScheduleQueryResource): CarrierScheduleQueryResource {
return {
...raw,
id: String(raw.id),
customerId: String(raw.customerId),
oceanBookingId: String(raw.oceanBookingId),
containerBookingId: optionalId(raw.containerBookingId),
resultPortScheduleId: optionalId(raw.resultPortScheduleId),
attemptCount: numberValue(raw.attemptCount),
version: numberValue(raw.version),
}
}
function view(raw: CarrierScheduleQueryView): CarrierScheduleQueryView {
return { ...raw, query: resource(raw.query), maxAttempts: numberValue(raw.maxAttempts), nextAllowedAt: raw.nextAllowedAt ?? raw.query.nextAllowedAt ?? null }
}
function attempt(raw: CarrierScheduleQueryAttempt): CarrierScheduleQueryAttempt {
return { ...raw, id: String(raw.id), queryId: String(raw.queryId), windowAttemptNo: numberValue(raw.windowAttemptNo), coverageGap: Boolean(raw.coverageGap) }
}
function history(raw: CarrierScheduleQueryHistory): CarrierScheduleQueryHistory {
return { ...raw, id: String(raw.id), queryId: String(raw.queryId), fromVersion: raw.fromVersion == null ? null : numberValue(raw.fromVersion), toVersion: raw.toVersion == null ? null : numberValue(raw.toVersion) }
}
export async function listCarrierScheduleQueries(filter: CarrierScheduleQueryFilter): Promise<CarrierScheduleQueryPage> {
const response = await http.get<ApiEnvelope<CarrierScheduleQueryPage>, ApiEnvelope<CarrierScheduleQueryPage>>(basePath, { params: filter })
const result = unwrapApiResponse(response)
return { ...result, records: (result.records || []).map(view), page: numberValue(result.page, filter.page), size: numberValue(result.size, filter.size), total: numberValue(result.total), pages: numberValue(result.pages) }
}
export async function getCarrierScheduleQuery(queryId: string): Promise<CarrierScheduleQueryDetail> {
const response = await http.get<ApiEnvelope<CarrierScheduleQueryDetail>, ApiEnvelope<CarrierScheduleQueryDetail>>(`${basePath}/${encodeURIComponent(queryId)}`)
const result = unwrapApiResponse(response)
return { ...result, query: view(result.query), attempts: (result.attempts || []).map(attempt), histories: (result.histories || []).map(history) }
}
@@ -0,0 +1,31 @@
import { readFileSync } from 'node:fs'
import { resolve } from 'node:path'
import { describe, expect, it } from 'vitest'
const source = readFileSync(resolve(process.cwd(), 'src/modules/port-schedules/components/CarrierScheduleQueryPanel.vue'), 'utf8')
describe('CarrierScheduleQueryPanel', () => {
it('uses the existing relation filters and never creates or executes polling tasks', () => {
expect(source).toContain('listCarrierScheduleQueries({ page: 1, size: 200')
expect(source).toContain('oceanBookingId: props.oceanBookingId')
expect(source).toContain('containerBookingId: props.containerBookingId')
expect(source).not.toContain('createCarrierSchedule')
expect(source).not.toContain('attemptCarrierSchedule')
})
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('query.lastErrorMessage')
expect(source).toContain("return '结果已过期'")
})
it('keeps the missing node-event contract explicit instead of fabricating data', () => {
expect(source).toContain('待接口提供独立节点事实')
expect(source).toContain('不会模拟节点或自动创建任务')
})
})
@@ -0,0 +1,90 @@
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
import { getCarrierScheduleQuery, listCarrierScheduleQueries } from '../api/platformCarrierScheduleQueries'
import { getPortSchedule } from '../api/platformPortSchedules'
import type { CarrierScheduleQueryDetail, CarrierScheduleQueryView, PortScheduleView } from '../types'
const props = defineProps<{ oceanBookingId?: string | null; containerBookingId?: string | null; portScheduleId?: string | null }>()
const loading = ref(false)
const error = ref<ApiErrorDetails | null>(null)
const detailError = ref<ApiErrorDetails | null>(null)
const selected = ref<CarrierScheduleQueryView | null>(null)
const detail = ref<CarrierScheduleQueryDetail | null>(null)
const latestSchedule = ref<PortScheduleView | null>(null)
const query = computed(() => detail.value?.query.query || selected.value?.query || null)
const queryView = computed(() => detail.value?.query || selected.value)
const statusText = computed(() => {
if (!query.value) return '未建立'
if (queryView.value?.expired) return '结果已过期'
return ({ READY: '待查询', MANUAL_REQUIRED: '需人工补充', SUCCESS: '最近查询成功' } as Record<string, string>)[query.value.status] || query.value.status
})
const statusType = computed<'success' | 'warning' | 'danger'>(() => queryView.value?.expired || query.value?.status === 'MANUAL_REQUIRED' ? 'danger' : query.value?.status === 'SUCCESS' ? 'success' : 'warning')
const latestSource = computed(() => latestSchedule.value?.schedule.sourceName || query.value?.carrierCode || '—')
const sourceReference = computed(() => latestSchedule.value?.schedule.sourceRef || query.value?.officialUrl || '—')
const latestObservation = computed(() => latestSchedule.value?.schedule.observedAt || query.value?.sourceUpdatedAt || query.value?.lastSuccessAt || null)
const histories = computed(() => [...(detail.value?.histories || [])].reverse())
const attempts = computed(() => [...(detail.value?.attempts || [])].reverse())
function formatTime(value?: string | null) { return value ? value.replace('T', ' ').slice(0, 16) : '—' }
function attemptText(status: string) { return ({ SUCCESS: '成功', FAILED: '失败', MANUAL_REQUIRED: '需人工补充' } as Record<string, string>)[status] || status }
function historyText(action: string) { return ({ CREATE: '建立查询任务', ATTEMPT: '执行查询', MANUAL_RESULT: '关联人工船期结果' } as Record<string, string>)[action] || action }
function latestTimestamp(item: CarrierScheduleQueryView) { return Date.parse(item.query.updatedAt || item.query.createdAt || '') || 0 }
async function load() {
selected.value = null; detail.value = null; latestSchedule.value = null; error.value = null; detailError.value = null
if (!props.oceanBookingId && !props.containerBookingId) return
loading.value = true
try {
const page = await listCarrierScheduleQueries({ page: 1, size: 200, oceanBookingId: props.oceanBookingId || undefined, containerBookingId: props.containerBookingId || undefined })
const matching = props.portScheduleId ? page.records.filter((item) => item.query.resultPortScheduleId === String(props.portScheduleId)) : page.records
selected.value = [...matching].sort((left, right) => latestTimestamp(right) - latestTimestamp(left))[0] || null
if (!selected.value) return
const tasks: [Promise<CarrierScheduleQueryDetail>, Promise<{ portSchedule: PortScheduleView } | null>] = [
getCarrierScheduleQuery(selected.value.query.id),
selected.value.query.resultPortScheduleId ? getPortSchedule(selected.value.query.resultPortScheduleId) : Promise.resolve(null),
]
const [queryResult, scheduleResult] = await Promise.allSettled(tasks)
if (queryResult.status === 'fulfilled') detail.value = queryResult.value
else detailError.value = getApiErrorDetails(queryResult.reason, '查询历史加载失败')
if (scheduleResult.status === 'fulfilled') latestSchedule.value = scheduleResult.value?.portSchedule || null
else detailError.value = getApiErrorDetails(scheduleResult.reason, '最新船期结果加载失败')
} catch (cause) { error.value = getApiErrorDetails(cause, '船节点查询状态加载失败') }
finally { loading.value = false }
}
watch(() => [props.oceanBookingId, props.containerBookingId, props.portScheduleId], () => void load(), { immediate: true })
</script>
<template>
<section class="carrier-query" aria-label="国内出仓后船节点">
<header><div><h2>国内出仓后船节点</h2><p>沿用 CarrierScheduleQuery,跟踪至目的港到达;本页面不会新建轮询任务。</p></div><el-tag v-if="query" :type="statusType" effect="light">{{ statusText }}</el-tag></header>
<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>
<el-empty v-else-if="!query" :image-size="52" description="尚未建立关联的船节点查询任务;当前页面不会模拟节点或自动创建任务。" />
<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="detailError" type="warning" :closable="false" show-icon title="部分查询详情暂不可用"><template #default><ApiErrorMeta :details="detailError" /></template></el-alert>
<dl class="query-facts">
<div><dt>查询任务</dt><dd>{{ query.queryNo || query.id }}</dd></div><div><dt>跟踪标识</dt><dd>{{ query.trackingNo || '—' }}</dd></div>
<div><dt>最新来源</dt><dd>{{ latestSource }}</dd></div><div><dt>来源参考</dt><dd class="break-value">{{ sourceReference }}</dd></div>
<div><dt>最新节点</dt><dd>{{ latestSchedule ? '船期快照已更新' : '待接口提供独立节点事实' }}</dd></div><div><dt>观测时间</dt><dd>{{ formatTime(latestObservation) }}</dd></div>
<div><dt>来源更新时间</dt><dd>{{ formatTime(query.sourceUpdatedAt) }}</dd></div><div><dt>任务更新时间</dt><dd>{{ formatTime(query.updatedAt) }}</dd></div>
<div><dt>最近成功</dt><dd>{{ formatTime(query.lastSuccessAt) }}</dd></div><div><dt>最近尝试</dt><dd>{{ formatTime(query.lastAttemptAt) }}</dd></div>
<div><dt>结果过期时间</dt><dd>{{ formatTime(query.expiresAt) }}</dd></div><div><dt>下次允许查询</dt><dd>{{ formatTime(queryView?.nextAllowedAt) }}</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 class="history-grid">
<section><h3>查询尝试</h3><p v-if="!attempts.length" class="empty-line">暂无查询尝试</p><ol v-else><li v-for="item in attempts" :key="item.id"><span>{{ formatTime(item.createdAt) }}</span><strong>{{ attemptText(item.attemptStatus) }}</strong><small>{{ item.errorMessage || item.manualReason || item.operatorName || item.carrierCode }}</small></li></ol></section>
<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.reason || item.operatorName || '系统记录' }}</small></li></ol></section>
</div>
</template>
</section>
</template>
<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}.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}.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 100px 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}@media(max-width:1200px){.query-facts{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:720px){.query-facts,.history-grid{grid-template-columns:1fr}.history-grid li{grid-template-columns:1fr}}
</style>
+1
View File
@@ -1,2 +1,3 @@
export { portScheduleRoutes } from './routes'
export { default as CarrierScheduleQueryPanel } from './components/CarrierScheduleQueryPanel.vue'
export * from './types'
@@ -84,4 +84,9 @@ describe('PortSchedulesView', () => {
expect(source).toContain('<el-button link type="primary" @click="loadCustomers"')
expect(source).toContain(':total="total" stripe row-key="schedule.id"')
})
it('shows query facts only inside the existing read-only schedule dialog', () => {
expect(source).toContain('CarrierScheduleQueryPanel')
expect(source).toContain('v-if="dialogReadonly && form.oceanBookingId"')
expect(source).toContain(':port-schedule-id="editingId"')
})
})
@@ -13,6 +13,7 @@ import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam'
import { listOceanBookings, type OceanBookingResource } from '@/modules/ocean-bookings'
import CarrierScheduleQueryPanel from '../components/CarrierScheduleQueryPanel.vue'
import { createPortSchedule, getPortSchedule, listPortSchedules, updatePortSchedule } from '../api/platformPortSchedules'
import type { PortScheduleData, PortScheduleView } from '../types'
@@ -63,7 +64,7 @@ watch([page, pageSize], () => void load()); onMounted(() => { void load(); void
<template #vessel="{ row }">{{ row.schedule.vesselName }} / {{ row.schedule.voyageNo }}</template><template #observedAt="{ row }">{{ formatDateTime(row.schedule.observedAt) }}</template><template #updatedAt="{ row }">{{ formatDateTime(row.schedule.updatedAt) }}</template><template #expired="{ row }"><el-tag :type="row.expired ? 'danger' : 'success'">{{ row.expired ? '已过期' : '有效' }}</el-tag></template>
</DataTable></section>
</ListPageLayout>
<AppDialog v-model="dialogOpen" :title="dialogTitle" width="820px" :confirm-loading="saving" :show-confirm="!dialogReadonly" :cancel-text="dialogReadonly ? '关闭' : '取消'" @confirm="save"><el-alert v-if="actionError" type="error" :closable="false"><ApiErrorMeta :details="actionError" /></el-alert><div v-if="customersError" class="selector-error"><span>{{ customersError.message }}</span><el-button link type="primary" @click="loadCustomers">重新加载客户</el-button></div><div v-if="bookingsError" class="selector-error"><span>{{ bookingsError.message }}</span><el-button link type="primary" @click="loadBookings()">重新加载订舱</el-button></div><el-form ref="formRef" :model="form" :rules="rules" :disabled="dialogReadonly" label-position="top" class="schedule-form"><el-row :gutter="16">
<AppDialog v-model="dialogOpen" :title="dialogTitle" width="820px" :confirm-loading="saving" :show-confirm="!dialogReadonly" :cancel-text="dialogReadonly ? '关闭' : '取消'" @confirm="save"><div class="schedule-dialog-body"><el-alert v-if="actionError" type="error" :closable="false"><ApiErrorMeta :details="actionError" /></el-alert><div v-if="customersError" class="selector-error"><span>{{ customersError.message }}</span><el-button link type="primary" @click="loadCustomers">重新加载客户</el-button></div><div v-if="bookingsError" class="selector-error"><span>{{ bookingsError.message }}</span><el-button link type="primary" @click="loadBookings()">重新加载订舱</el-button></div><el-form ref="formRef" :model="form" :rules="rules" :disabled="dialogReadonly" label-position="top" class="schedule-form"><el-row :gutter="16">
<el-col :xs="24" :sm="12"><el-form-item label="客户" prop="customerId"><el-select :model-value="form.customerId" filterable placeholder="请选择客户" :loading="customersLoading" no-data-text="暂无可用客户" style="width:100%" @update:model-value="handleCustomerChange(String($event || ''))"><el-option v-for="customer in selectableCustomers" :key="String(customer.id)" :label="customerLabel(customer)" :value="String(customer.id)" /></el-select></el-form-item></el-col>
<el-col :xs="24" :sm="12"><el-form-item label="订舱记录" prop="oceanBookingId"><el-select v-model="form.oceanBookingId" filterable placeholder="请先选择客户,再选择订舱记录" :loading="bookingsLoading" :disabled="!form.customerId" no-data-text="该客户暂无订舱记录" style="width:100%"><el-option v-for="booking in bookings" :key="booking.id" :label="bookingLabel(booking)" :value="booking.id" /></el-select></el-form-item></el-col>
<el-col :xs="24" :sm="12"><el-form-item label="信息来源" prop="sourceName"><el-input v-model="form.sourceName" placeholder="如美森官网" /></el-form-item></el-col><el-col :xs="24" :sm="12"><el-form-item label="来源参考" prop="sourceRef"><el-input v-model="form.sourceRef" placeholder="查询链接或参考号" /></el-form-item></el-col>
@@ -72,6 +73,6 @@ watch([page, pageSize], () => void load()); onMounted(() => { void load(); void
<el-col :xs="24" :sm="12"><el-form-item label="预计开船"><el-date-picker v-model="form.etdAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="请选择预计开船时间" style="width:100%" /></el-form-item></el-col><el-col :xs="24" :sm="12"><el-form-item label="预计到港"><el-date-picker v-model="form.etaAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="请选择预计到港时间" style="width:100%" /></el-form-item></el-col>
<el-col :xs="24" :sm="12"><el-form-item label="查询时间" prop="observedAt"><el-date-picker v-model="form.observedAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="请选择本次查询时间" style="width:100%" /></el-form-item></el-col><el-col :xs="24" :sm="12"><el-form-item label="过期时间" prop="expiresAt"><el-date-picker v-model="form.expiresAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="请选择结果过期时间" style="width:100%" /></el-form-item></el-col>
<el-col :xs="24"><el-form-item label="备注"><el-input v-model="form.remark" type="textarea" :rows="3" maxlength="500" show-word-limit placeholder="请输入船期补充说明(可选)" /></el-form-item></el-col>
</el-row></el-form></AppDialog></AdminPage>
</el-row></el-form><CarrierScheduleQueryPanel v-if="dialogReadonly && form.oceanBookingId" class="schedule-query-panel" :ocean-booking-id="form.oceanBookingId" :container-booking-id="form.containerBookingId" :port-schedule-id="editingId" /></div></AppDialog></AdminPage>
</template>
<style scoped>.schedule-form{max-height:65vh;overflow:auto;padding-inline:2px}.selector-error{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px;padding:6px 10px;border-radius:var(--cc-radius-sm);background:var(--cc-danger-soft);color:var(--el-color-danger);font-size:12px;line-height:20px}.selector-error span{overflow-wrap:anywhere}:global(.el-dialog:has(.schedule-form)){width:min(820px,calc(100vw - 28px))!important;max-height:90vh;overflow:hidden}@media(max-width:720px){.schedule-form{max-height:68vh}}</style>
<style scoped>.schedule-dialog-body{max-height:65vh;overflow:auto;padding-inline:2px}.schedule-query-panel{margin-top:4px;padding-top:16px;border-top:1px solid var(--cc-border)}.selector-error{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px;padding:6px 10px;border-radius:var(--cc-radius-sm);background:var(--cc-danger-soft);color:var(--el-color-danger);font-size:12px;line-height:20px}.selector-error span{overflow-wrap:anywhere}:global(.el-dialog:has(.schedule-form)){width:min(820px,calc(100vw - 28px))!important;max-height:90vh;overflow:hidden}@media(max-width:720px){.schedule-dialog-body{max-height:68vh}}</style>
+88
View File
@@ -32,3 +32,91 @@ export interface PortSchedulePage { records: PortScheduleView[]; page: number; s
export interface PortScheduleQuery { page: number; size: number; customerId?: string; keyword?: string; expired?: boolean }
export type PortScheduleData = Omit<PortScheduleResource, 'id' | 'scheduleNo' | 'version' | 'updatedAt'>
export interface PortScheduleSaveRequest { expectedVersion: number; data: PortScheduleData; reason?: string | null }
export type CarrierScheduleQueryStatus = 'READY' | 'MANUAL_REQUIRED' | 'SUCCESS'
export interface CarrierScheduleQueryResource {
id: string
queryNo: string
customerId: string
oceanBookingId: string
containerBookingId?: string | null
carrierCode: string
trackingType: string
trackingNo: string
officialUrl?: string | null
accessMode?: string | null
status: CarrierScheduleQueryStatus
windowStartedAt?: string | null
attemptCount: number
nextAllowedAt?: string | null
lastAttemptAt?: string | null
lastSuccessAt?: string | null
sourceUpdatedAt?: string | null
expiresAt?: string | null
resultPortScheduleId?: string | null
lastAttemptStatus?: string | null
manualReason?: string | null
lastErrorCode?: string | null
lastErrorMessage?: string | null
remark?: string | null
createdAt?: string | null
updatedAt?: string | null
version: number
}
export interface CarrierScheduleQueryView {
query: CarrierScheduleQueryResource
expired: boolean
maxAttempts: number
nextAllowedAt?: string | null
}
export interface CarrierScheduleQueryAttempt {
id: string
queryId: string
windowAttemptNo: number
carrierCode: string
accessMode?: string | null
officialUrl?: string | null
attemptStatus: string
coverageGap?: boolean | null
coverageGapCode?: string | null
manualReason?: string | null
errorCode?: string | null
errorMessage?: string | null
operatorName?: string | null
createdAt?: string | null
}
export interface CarrierScheduleQueryHistory {
id: string
queryId: string
action: string
fromVersion?: number | null
toVersion?: number | null
reason?: string | null
operatorName?: string | null
createdAt?: string | null
}
export interface CarrierScheduleQueryDetail {
query: CarrierScheduleQueryView
attempts: CarrierScheduleQueryAttempt[]
histories: CarrierScheduleQueryHistory[]
}
export interface CarrierScheduleQueryPage {
records: CarrierScheduleQueryView[]
page: number
size: number
total: number
pages: number
}
export interface CarrierScheduleQueryFilter {
page: number
size: number
oceanBookingId?: string
containerBookingId?: string
}