优化
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
|
||||
import type { CustomsCaseCreateRequest, CustomsCaseDetail, CustomsCasePage, CustomsCaseQuery, CustomsCaseResource } from '../types'
|
||||
import type { CustomsCaseCreateRequest, CustomsCaseDetail, CustomsCasePage, CustomsCaseQuery, CustomsCaseResource, CustomsSubmitBatchRequest } 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<string, unknown>; for (const key of keys) if (value[key] != null) value[key] = String(value[key]); value.version = Number(value.version ?? 0); return value }
|
||||
@@ -8,4 +8,4 @@ function idem() { return `customs-${Date.now()}-${Math.random().toString(36).sli
|
||||
export async function listCustomsCases(query: CustomsCaseQuery) { const response = await http.get<ApiEnvelope<CustomsCasePage>, ApiEnvelope<CustomsCasePage>>(`${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<CustomsCaseDetail>, ApiEnvelope<CustomsCaseDetail>>(`${basePath}/cases/${encodeURIComponent(id)}`); return detail(unwrapApiResponse(response)) }
|
||||
export async function createCustomsCase(request: CustomsCaseCreateRequest) { const response = await http.post<ApiEnvelope<CustomsCaseDetail>, ApiEnvelope<CustomsCaseDetail>>(`${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<unknown>, ApiEnvelope<unknown>>(`${basePath}/cases/${encodeURIComponent(id)}/submission-batches`, request, { headers: { 'Idempotency-Key': idem() } }); return unwrapApiResponse(response) }
|
||||
export async function submitCustomsCase(id: string, request: CustomsSubmitBatchRequest) { const response = await http.post<ApiEnvelope<unknown>, ApiEnvelope<unknown>>(`${basePath}/cases/${encodeURIComponent(id)}/submission-batches`, request, { headers: { 'Idempotency-Key': idem() } }); return unwrapApiResponse(response) }
|
||||
|
||||
@@ -14,11 +14,13 @@ import { customsCaseStatusOptions, customsHistoryActionLabel, customsStatusLabel
|
||||
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam'
|
||||
import { listSuppliers, type Supplier } from '@/modules/suppliers'
|
||||
import { getCustomsCase, listCustomsCases, submitCustomsCase } from '../api/platformCustoms'
|
||||
import type { CustomsCaseDetail, CustomsCaseResource } from '../types'
|
||||
|
||||
const access = useAccessPreviewStore(); const rows = ref<CustomsCaseResource[]>([]); const loading = ref(false); const error = ref<ApiErrorDetails | null>(null); const page = ref(1); const pageSize = ref(20); const total = ref(0)
|
||||
const customers = ref<PlatformTenantResource[]>([]); const customersLoading = ref(false)
|
||||
const customsAgents = ref<Supplier[]>([]); const customsAgentsLoading = ref(false); const customsAgentsError = ref('')
|
||||
const filters = ref<QueryModel>({ customerId: '', status: '' }); const fields = computed<QueryField[]>(() => [
|
||||
{ key: 'customerId', type: 'select', label: '客户', placeholder: '全部客户', width: 220, options: customers.value.filter((customer) => customer.status === 'ACTIVE').map((customer) => ({ label: customer.tenantName, value: String(customer.id) })), props: { filterable: true, loading: customersLoading.value } },
|
||||
{ key: 'status', type: 'select', label: '状态', options: customsCaseStatusOptions.map((option) => ({ ...option })) },
|
||||
@@ -26,13 +28,15 @@ const filters = ref<QueryModel>({ customerId: '', status: '' }); const fields =
|
||||
const columns = [{ key: 'actions', label: '操作', fixed: 'left', width: 175, slotName: 'actions', hideable: false, movable: false }, { prop: 'customsNo', label: '报关编号', minWidth: 145 }, { key: 'orderLink', label: '订单关联', minWidth: 120, formatter: (row: CustomsCaseResource) => associationLabel(row.orderId) }, { key: 'inboundLink', label: '入库单关联', minWidth: 120, formatter: (row: CustomsCaseResource) => associationLabel(row.inboundId) }, { prop: 'checklistCode', label: '资料清单', minWidth: 125 }, { prop: 'checklistVersionNo', label: '版本', width: 90 }, { prop: 'status', label: '状态', width: 110, slotName: 'status' }, { prop: 'updatedAt', label: '更新时间', minWidth: 160 }]
|
||||
function associationLabel(value: string | null | undefined) { return value ? '已关联' : '未关联' }
|
||||
async function loadCustomers() { customersLoading.value = true; try { const result = await listPlatformTenants({ page: 1, size: 200, status: 'ACTIVE' }); customers.value = result.records.filter((customer) => customer.status === 'ACTIVE') } catch { customers.value = [] } finally { customersLoading.value = false } }
|
||||
async function loadCustomsAgents() { customsAgentsLoading.value = true; customsAgentsError.value = ''; try { const result = await listSuppliers({ page: 1, size: 200, category: 'CUSTOMS', status: 'ACTIVE' }); customsAgents.value = result.records.filter((supplier) => supplier.category === 'CUSTOMS' && supplier.status === 'ACTIVE') } catch (cause) { customsAgents.value = []; customsAgentsError.value = getApiErrorDetails(cause, '报关代理加载失败').message } finally { customsAgentsLoading.value = false } }
|
||||
async function load() { loading.value = true; error.value = null; try { const result = await listCustomsCases({ page: page.value, size: pageSize.value, customerId: String(filters.value.customerId || '') || undefined, status: String(filters.value.status || '') || undefined }); rows.value = result.records; total.value = result.total } catch (cause) { error.value = getApiErrorDetails(cause) } finally { loading.value = false } }
|
||||
function search() { if (page.value === 1) void load(); else page.value = 1 }
|
||||
const detailOpen = ref(false); const detail = ref<CustomsCaseDetail | null>(null); const detailLoading = ref(false); const actionError = ref<ApiErrorDetails | null>(null); const submitForm = reactive({ customsAgentName: '', externalReference: '', note: '' }); const canSubmit = computed(() => detail.value?.customsCase.status === 'READY_TO_SUBMIT' && access.can('platform:firstleg:customs:submit'))
|
||||
async function openDetail(id: string) { detailOpen.value = true; detailLoading.value = true; actionError.value = null; try { detail.value = await getCustomsCase(id) } catch (cause) { actionError.value = getApiErrorDetails(cause) } finally { detailLoading.value = false } }
|
||||
async function submit() { if (!detail.value || !submitForm.customsAgentName.trim()) return; actionError.value = null; try { await submitCustomsCase(detail.value.customsCase.id, { ...submitForm, expectedVersion: detail.value.customsCase.version }); ElMessage.success('已登记线下提交批次'); detail.value = await getCustomsCase(detail.value.customsCase.id) } catch (cause) { actionError.value = getApiErrorDetails(cause); if (actionError.value.code === 'A0409') detail.value = await getCustomsCase(detail.value.customsCase.id) } }
|
||||
watch([page, pageSize], () => void load()); onMounted(() => { void load(); void loadCustomers() })
|
||||
const detailOpen = ref(false); const detail = ref<CustomsCaseDetail | null>(null); const detailLoading = ref(false); const actionError = ref<ApiErrorDetails | null>(null); const submitForm = reactive({ customsAgentId: '', customsAgentName: '', externalReference: '', note: '' }); const canSubmit = computed(() => detail.value?.customsCase.status === 'READY_TO_SUBMIT' && access.can('platform:firstleg:customs:submit'))
|
||||
function selectCustomsAgent(supplierId: string) { const supplier = customsAgents.value.find((item) => item.id === supplierId); submitForm.customsAgentId = supplier?.id || ''; submitForm.customsAgentName = supplier?.name || '' }
|
||||
async function openDetail(id: string) { detailOpen.value = true; detailLoading.value = true; actionError.value = null; selectCustomsAgent(''); try { detail.value = await getCustomsCase(id) } catch (cause) { actionError.value = getApiErrorDetails(cause) } finally { detailLoading.value = false } }
|
||||
async function submit() { if (!detail.value || !submitForm.customsAgentId || !customsAgents.value.some((item) => item.id === submitForm.customsAgentId)) { actionError.value = { message: '请选择有效的合作中报关供应商' }; return } actionError.value = null; try { await submitCustomsCase(detail.value.customsCase.id, { ...submitForm, expectedVersion: detail.value.customsCase.version }); ElMessage.success('已登记线下提交批次'); detail.value = await getCustomsCase(detail.value.customsCase.id) } catch (cause) { actionError.value = getApiErrorDetails(cause); if (actionError.value.code === 'A0409') detail.value = await getCustomsCase(detail.value.customsCase.id) } }
|
||||
watch([page, pageSize], () => void load()); onMounted(() => { void load(); void loadCustomers(); void loadCustomsAgents() })
|
||||
</script>
|
||||
<template><AdminPage as="div"><ListPageLayout aria-label="报关协同列表"><template #toolbar><QueryToolbar v-model="filters" class="surface-panel" ariaLabel="报关协同筛选" :fields="fields" @search="search" /></template><template v-if="error" #state><LoadErrorState title="报关协同加载失败" :message="error.message" :loading="loading" @retry="load"><template #details><ApiErrorMeta :details="error" /></template></LoadErrorState></template><section v-if="!error" class="surface-panel table-panel"><DataTable v-model:page="page" v-model:page-size="pageSize" :data="rows" :columns="columns" :loading="loading" :total="total" stripe row-key="id" preference-key="customs-cases" empty-text="暂无报关协同记录"><template #actions="{ row }"><el-button v-if="access.can('platform:firstleg:customs:view')" size="small" type="primary" plain @click="openDetail(row.id)"><Eye :size="15" />查看</el-button></template><template #status="{ row }"><StatusBadge :status="customsStatusLabel(row.status)" /></template></DataTable></section></ListPageLayout>
|
||||
<AppDialog v-model="detailOpen" title="报关协同详情" width="900px" :show-confirm="false"><LoadErrorState v-if="actionError && !detail" title="详情加载失败" :message="actionError.message" :loading="detailLoading" :show-retry="false"><template #details><ApiErrorMeta :details="actionError" /></template></LoadErrorState><template v-if="detail"><el-alert v-if="actionError" type="error" :closable="false"><ApiErrorMeta :details="actionError" /></el-alert><el-descriptions :column="2" border><el-descriptions-item label="报关编号">{{ detail.customsCase.customsNo }}</el-descriptions-item><el-descriptions-item label="状态"><StatusBadge :status="customsStatusLabel(detail.customsCase.status)" /></el-descriptions-item><el-descriptions-item label="资料清单">{{ detail.customsCase.checklistCode }} / {{ detail.customsCase.checklistVersionNo }}</el-descriptions-item><el-descriptions-item label="资料/回执">{{ detail.documents.length }} / {{ detail.receipts.length }}</el-descriptions-item></el-descriptions><el-form v-if="canSubmit" label-position="top" class="submit-form"><el-row :gutter="12"><el-col :xs="24" :sm="12"><el-form-item label="报关代理"><el-input v-model="submitForm.customsAgentName" placeholder="请输入线下报关代理名称" /></el-form-item></el-col><el-col :xs="24" :sm="12"><el-form-item label="外部参考号"><el-input v-model="submitForm.externalReference" placeholder="请输入代理回传的参考号(可选)" /></el-form-item></el-col><el-col :xs="24"><el-form-item label="提交备注"><el-input v-model="submitForm.note" placeholder="请输入本次提交说明(可选)" /></el-form-item></el-col></el-row><el-button type="primary" @click="submit"><Send :size="16" />登记线下提交</el-button></el-form><h3>状态历史</h3><el-timeline><el-timeline-item v-for="entry in detail.history" :key="entry.id" :timestamp="entry.createdAt">{{ customsHistoryActionLabel(entry.action) }}:{{ customsStatusLabel(entry.fromStatus) }} → {{ customsStatusLabel(entry.toStatus) }}<small>{{ entry.note || '' }}</small></el-timeline-item></el-timeline></template></AppDialog></AdminPage></template>
|
||||
<AppDialog v-model="detailOpen" title="报关协同详情" width="900px" :show-confirm="false"><LoadErrorState v-if="actionError && !detail" title="详情加载失败" :message="actionError.message" :loading="detailLoading" :show-retry="false"><template #details><ApiErrorMeta :details="actionError" /></template></LoadErrorState><template v-if="detail"><el-alert v-if="actionError" type="error" :closable="false"><ApiErrorMeta :details="actionError" /></el-alert><el-descriptions :column="2" border><el-descriptions-item label="报关编号">{{ detail.customsCase.customsNo }}</el-descriptions-item><el-descriptions-item label="状态"><StatusBadge :status="customsStatusLabel(detail.customsCase.status)" /></el-descriptions-item><el-descriptions-item label="资料清单">{{ detail.customsCase.checklistCode }} / {{ detail.customsCase.checklistVersionNo }}</el-descriptions-item><el-descriptions-item label="资料/回执">{{ detail.documents.length }} / {{ detail.receipts.length }}</el-descriptions-item></el-descriptions><el-form v-if="canSubmit" label-position="top" class="submit-form"><el-alert v-if="customsAgentsError" type="warning" :closable="false" :title="customsAgentsError"><el-button link type="warning" :loading="customsAgentsLoading" @click="loadCustomsAgents">重新加载</el-button></el-alert><el-row :gutter="12"><el-col :xs="24" :sm="12"><el-form-item label="报关代理" required><el-select :model-value="submitForm.customsAgentId" filterable clearable placeholder="请选择报关代理" :loading="customsAgentsLoading" no-data-text="暂无可用报关供应商" style="width:100%" @update:model-value="selectCustomsAgent(String($event || ''))"><el-option v-for="supplier in customsAgents" :key="supplier.id" :label="supplier.name" :value="supplier.id" /></el-select></el-form-item></el-col><el-col :xs="24" :sm="12"><el-form-item label="外部参考号"><el-input v-model="submitForm.externalReference" placeholder="请输入代理回传的参考号(可选)" /></el-form-item></el-col><el-col :xs="24"><el-form-item label="提交备注"><el-input v-model="submitForm.note" placeholder="请输入本次提交说明(可选)" /></el-form-item></el-col></el-row><el-button type="primary" :disabled="!submitForm.customsAgentId" @click="submit"><Send :size="16" />登记线下提交</el-button></el-form><h3>提交批次</h3><el-empty v-if="!detail.submissionBatches.length" description="暂无提交批次" :image-size="56" /><el-timeline v-else><el-timeline-item v-for="batch in detail.submissionBatches" :key="batch.id" :timestamp="batch.createdAt">报关代理:{{ batch.customsAgentName || '服务端未返回名称快照' }}</el-timeline-item></el-timeline><h3>状态历史</h3><el-timeline><el-timeline-item v-for="entry in detail.history" :key="entry.id" :timestamp="entry.createdAt">{{ customsHistoryActionLabel(entry.action) }}:{{ customsStatusLabel(entry.fromStatus) }} → {{ customsStatusLabel(entry.toStatus) }}<small>{{ entry.note || '' }}</small></el-timeline-item></el-timeline></template></AppDialog></AdminPage></template>
|
||||
<style scoped>.submit-form{margin-top:18px}.submit-form+.el-button{margin-bottom:12px}h3{margin:20px 0 12px}small{display:block;color:var(--cc-muted)}:global(.el-dialog:has(.submit-form,.el-descriptions)){width:min(900px,calc(100vw - 28px))!important;max-height:90vh;overflow:hidden}:global(.el-dialog:has(.submit-form,.el-descriptions) .el-dialog__body){max-height:calc(90vh - 120px);overflow-y:auto}</style>
|
||||
|
||||
@@ -2,6 +2,7 @@ export interface CustomsCaseResource { id: string; customsNo: string; tenantId?:
|
||||
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 CustomsSubmitBatchRequest { customsAgentId: string; customsAgentName: string; externalReference?: string; note?: string; expectedVersion: number }
|
||||
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 }
|
||||
|
||||
Reference in New Issue
Block a user