From c8eb2d75cdcadde15a1d3d58270c3c192d6c86d0 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Tue, 11 Aug 2026 11:26:31 +0800 Subject: [PATCH] feat(admin): add phase3 service center pages --- .../customs/api/platformCustoms.test.ts | 5 ++ src/modules/customs/api/platformCustoms.ts | 11 ++++ src/modules/customs/index.ts | 2 + .../customs/pages/CustomsCasesView.test.ts | 5 ++ .../customs/pages/CustomsCasesView.vue | 34 +++++++++++ src/modules/customs/routes.ts | 2 + src/modules/customs/types.ts | 7 +++ .../pages/OceanBookingDetailView.vue | 5 +- .../pages/OceanBookingsView.vue | 3 +- .../api/platformOutboundDispatch.test.ts | 5 ++ .../api/platformOutboundDispatch.ts | 9 +++ src/modules/outbound-dispatch/index.ts | 2 + .../pages/OutboundDispatchesView.test.ts | 5 ++ .../pages/OutboundDispatchesView.vue | 28 +++++++++ src/modules/outbound-dispatch/routes.ts | 2 + src/modules/outbound-dispatch/types.ts | 5 ++ .../api/platformPortSchedules.test.ts | 6 ++ .../api/platformPortSchedules.ts | 24 ++++++++ src/modules/port-schedules/index.ts | 2 + .../pages/PortSchedulesView.test.ts | 5 ++ .../pages/PortSchedulesView.vue | 60 +++++++++++++++++++ src/modules/port-schedules/routes.ts | 2 + src/modules/port-schedules/types.ts | 34 +++++++++++ 23 files changed, 260 insertions(+), 3 deletions(-) create mode 100644 src/modules/customs/api/platformCustoms.test.ts create mode 100644 src/modules/customs/api/platformCustoms.ts create mode 100644 src/modules/customs/index.ts create mode 100644 src/modules/customs/pages/CustomsCasesView.test.ts create mode 100644 src/modules/customs/pages/CustomsCasesView.vue create mode 100644 src/modules/customs/routes.ts create mode 100644 src/modules/customs/types.ts create mode 100644 src/modules/outbound-dispatch/api/platformOutboundDispatch.test.ts create mode 100644 src/modules/outbound-dispatch/api/platformOutboundDispatch.ts create mode 100644 src/modules/outbound-dispatch/index.ts create mode 100644 src/modules/outbound-dispatch/pages/OutboundDispatchesView.test.ts create mode 100644 src/modules/outbound-dispatch/pages/OutboundDispatchesView.vue create mode 100644 src/modules/outbound-dispatch/routes.ts create mode 100644 src/modules/outbound-dispatch/types.ts create mode 100644 src/modules/port-schedules/api/platformPortSchedules.test.ts create mode 100644 src/modules/port-schedules/api/platformPortSchedules.ts create mode 100644 src/modules/port-schedules/index.ts create mode 100644 src/modules/port-schedules/pages/PortSchedulesView.test.ts create mode 100644 src/modules/port-schedules/pages/PortSchedulesView.vue create mode 100644 src/modules/port-schedules/routes.ts create mode 100644 src/modules/port-schedules/types.ts diff --git a/src/modules/customs/api/platformCustoms.test.ts b/src/modules/customs/api/platformCustoms.test.ts new file mode 100644 index 00000000..8ac3c5eb --- /dev/null +++ b/src/modules/customs/api/platformCustoms.test.ts @@ -0,0 +1,5 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { http } from '@/shared/api/http' +import { createCustomsCase, listCustomsCases, submitCustomsCase } from './platformCustoms' +vi.mock('@/shared/api/http', async () => { const actual = await vi.importActual('@/shared/api/http'); return { ...actual, http: { get: vi.fn(), post: vi.fn() } } }) +describe('platformCustoms', () => { beforeEach(() => vi.clearAllMocks()); it('normalizes case ids', async () => { vi.mocked(http.get).mockResolvedValue({ code: '200', data: { records: [{ id: 1, customsNo: 'C1', orderId: 2, inboundId: 3, checklistVersionId: 4, checklistCode: 'US', checklistVersionNo: 'V1', status: 'DRAFT', version: '1' }], page: 1, size: 20, total: 1, pages: 1 } }); const page = await listCustomsCases({ page: 1, size: 20 }); expect(page.records[0]).toMatchObject({ id: '1', orderId: '2', inboundId: '3', checklistVersionId: '4', version: 1 }) }); it('writes with idempotency and version', async () => { const detail = { customsCase: { id: 1, customsNo: 'C1', orderId: 2, inboundId: 3, checklistVersionId: 4, checklistCode: 'US', checklistVersionNo: 'V1', status: 'DRAFT', version: 0 }, documents: [], supplements: [], submissionBatches: [], receipts: [], history: [] }; vi.mocked(http.post).mockResolvedValueOnce({ code: '200', data: detail }).mockResolvedValueOnce({ code: '200', data: {} }); await createCustomsCase({ customerId: '8', orderId: '2', inboundId: '3', checklistVersionId: '4', expectedVersion: 0 }); await submitCustomsCase('1', { customsAgentName: '代理', expectedVersion: 2 }); expect(vi.mocked(http.post).mock.calls[0]?.[2]).toMatchObject({ headers: { 'Idempotency-Key': expect.any(String) } }); expect(vi.mocked(http.post).mock.calls[1]?.[1]).toMatchObject({ expectedVersion: 2 }) }) }) diff --git a/src/modules/customs/api/platformCustoms.ts b/src/modules/customs/api/platformCustoms.ts new file mode 100644 index 00000000..9061af92 --- /dev/null +++ b/src/modules/customs/api/platformCustoms.ts @@ -0,0 +1,11 @@ +import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http' +import type { CustomsCaseCreateRequest, CustomsCaseDetail, CustomsCasePage, CustomsCaseQuery, CustomsCaseResource } from '../types' +const basePath = '/platform/v1/customs' +const keys = ['id', 'tenantId', 'orderId', 'inboundId', 'containerBookingId', 'checklistVersionId'] as const +function item(raw: CustomsCaseResource) { const value = { ...raw } as CustomsCaseResource & Record; for (const key of keys) if (value[key] != null) value[key] = String(value[key]); value.version = Number(value.version ?? 0); return value } +function detail(raw: CustomsCaseDetail): CustomsCaseDetail { return { ...raw, customsCase: item(raw.customsCase), documents: (raw.documents || []).map((x) => ({ ...x, id: String(x.id) })), submissionBatches: (raw.submissionBatches || []).map((x) => ({ ...x, id: String(x.id) })), receipts: (raw.receipts || []).map((x) => ({ ...x, id: String(x.id) })), history: (raw.history || []).map((x) => ({ ...x, id: String(x.id) })) } } +function idem() { return `customs-${Date.now()}-${Math.random().toString(36).slice(2)}` } +export async function listCustomsCases(query: CustomsCaseQuery) { const response = await http.get, ApiEnvelope>(`${basePath}/cases`, { params: query }); const data = unwrapApiResponse(response); return { ...data, records: (data.records || []).map(item), page: Number(data.page), size: Number(data.size), total: Number(data.total), pages: Number(data.pages) } } +export async function getCustomsCase(id: string) { const response = await http.get, ApiEnvelope>(`${basePath}/cases/${encodeURIComponent(id)}`); return detail(unwrapApiResponse(response)) } +export async function createCustomsCase(request: CustomsCaseCreateRequest) { const response = await http.post, ApiEnvelope>(`${basePath}/cases`, request, { headers: { 'Idempotency-Key': idem() } }); return detail(unwrapApiResponse(response)) } +export async function submitCustomsCase(id: string, request: { customsAgentName: string; externalReference?: string; note?: string; expectedVersion: number }) { const response = await http.post, ApiEnvelope>(`${basePath}/cases/${encodeURIComponent(id)}/submission-batches`, request, { headers: { 'Idempotency-Key': idem() } }); return unwrapApiResponse(response) } diff --git a/src/modules/customs/index.ts b/src/modules/customs/index.ts new file mode 100644 index 00000000..e5f14b6e --- /dev/null +++ b/src/modules/customs/index.ts @@ -0,0 +1,2 @@ +export { customsRoutes } from './routes' +export * from './types' diff --git a/src/modules/customs/pages/CustomsCasesView.test.ts b/src/modules/customs/pages/CustomsCasesView.test.ts new file mode 100644 index 00000000..025cad96 --- /dev/null +++ b/src/modules/customs/pages/CustomsCasesView.test.ts @@ -0,0 +1,5 @@ +import { describe, expect, it } from 'vitest' +import { readFileSync } from 'node:fs' +import { resolve } from 'node:path' +const source = readFileSync(resolve(process.cwd(), 'src/modules/customs/pages/CustomsCasesView.vue'), 'utf8') +describe('CustomsCasesView', () => { it('uses shared list and dialog components', () => { expect(source).toContain('QueryToolbar'); expect(source).toContain('DataTable'); expect(source).toContain('AppDialog'); expect(source).toContain("fixed: 'left'") }); it('supports create, detail, history and agent submission', () => { expect(source).toContain('createCustomsCase'); expect(source).toContain('getCustomsCase'); expect(source).toContain('submitCustomsCase'); expect(source).toContain('detail.history') }) }) diff --git a/src/modules/customs/pages/CustomsCasesView.vue b/src/modules/customs/pages/CustomsCasesView.vue new file mode 100644 index 00000000..fa8ddccf --- /dev/null +++ b/src/modules/customs/pages/CustomsCasesView.vue @@ -0,0 +1,34 @@ + + + diff --git a/src/modules/customs/routes.ts b/src/modules/customs/routes.ts new file mode 100644 index 00000000..f2eb6622 --- /dev/null +++ b/src/modules/customs/routes.ts @@ -0,0 +1,2 @@ +import type { RouteRecordRaw } from 'vue-router' +export const customsRoutes = [{ path: 'customs', name: 'customs-cases', component: () => import('./pages/CustomsCasesView.vue'), meta: { title: '报关协同', description: '维护报关资料、代理提交与放行回执', permission: 'platform:firstleg:customs:list' } }] satisfies RouteRecordRaw[] diff --git a/src/modules/customs/types.ts b/src/modules/customs/types.ts new file mode 100644 index 00000000..3692c9c4 --- /dev/null +++ b/src/modules/customs/types.ts @@ -0,0 +1,7 @@ +export interface CustomsCaseResource { id: string; customsNo: string; tenantId?: string | null; orderId: string; inboundId: string; containerBookingId?: string | null; checklistVersionId: string; checklistCode: string; checklistVersionNo: string; status: string; remark?: string | null; version: number; updatedAt?: string | null } +export interface CustomsDocumentResource { id: string; itemCode: string; fileName?: string | null; status?: string | null; version?: number } +export interface CustomsHistoryResource { id: string; action: string; fromStatus?: string | null; toStatus?: string | null; note?: string | null; operatorName?: string | null; createdAt: string } +export interface CustomsCaseDetail { customsCase: CustomsCaseResource; documents: CustomsDocumentResource[]; supplements: unknown[]; submissionBatches: Array<{ id: string; customsAgentName?: string; status?: string; createdAt?: string }>; receipts: Array<{ id: string; receiptType?: string; status?: string; createdAt?: string }>; history: CustomsHistoryResource[] } +export interface CustomsCasePage { records: CustomsCaseResource[]; page: number; size: number; total: number; pages: number } +export interface CustomsCaseQuery { page: number; size: number; customerId?: string; orderId?: string; inboundId?: string; containerBookingId?: string; status?: string } +export interface CustomsCaseCreateRequest { customerId: string; orderId: string; inboundId: string; containerBookingId?: string | null; checklistVersionId: string; remark?: string | null; expectedVersion: number } diff --git a/src/modules/ocean-bookings/pages/OceanBookingDetailView.vue b/src/modules/ocean-bookings/pages/OceanBookingDetailView.vue index c20d5fde..2d4fb616 100644 --- a/src/modules/ocean-bookings/pages/OceanBookingDetailView.vue +++ b/src/modules/ocean-bookings/pages/OceanBookingDetailView.vue @@ -6,6 +6,7 @@ import { ArrowLeft, Plus, Save, Trash2 } from '@lucide/vue' import { useAccessPreviewStore } from '@/core/access/accessPreview' import { getApiErrorDetails } from '@/shared/api/http' import LoadErrorState from '@/shared/components/LoadErrorState.vue' +import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue' import StatusBadge from '@/shared/components/StatusBadge.vue' import AdminPage from '@/shared/components/AdminPage.vue' import AppDialog from '@/shared/components/AppDialog.vue' @@ -199,9 +200,9 @@ onMounted(load) - +