@@ -10,6 +10,7 @@ import LoadErrorState from '@/shared/components/LoadErrorState.vue'
import AdminPage from '@/shared/components/AdminPage.vue'
import ListPageLayout from '@/shared/components/ListPageLayout.vue'
import StatusBadge from '@/shared/components/StatusBadge.vue'
import { customsCaseStatusOptions , customsHistoryActionLabel , customsStatusLabel } from '../status'
import { getApiErrorDetails , type ApiErrorDetails } from '@/shared/api/http'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import { listPlatformTenants , type PlatformTenantResource } from '@/modules/iam'
@@ -19,7 +20,7 @@ import { createCustomsCase, getCustomsCase, listCustomsCases, submitCustomsCase
import type { CustomsCaseCreateRequest , 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 filters = reactive < QueryModel > ( { orderId : '' , inboundId : '' , status : '' } ) ; const fields : QueryField [ ] = [ { key : 'orderId' , type : 'input' , label : '订单 ID' , width : 170 } , { key : 'inboundId' , type : 'input' , label : '入库单 ID' , width : 170 } , { key : 'status' , type : 'select' , label : '状态' , options : [ 'DRAFT' , 'COLLECTING' , 'READY' , 'SUBMITTED' , 'RELEASED' , 'REJECTED' ] . map ( ( value ) => ( { label : value , value } ) ) } ]
const filters = reactive < QueryModel > ( { orderId : '' , inboundId : '' , status : '' } ) ; const fields : QueryField [ ] = [ { key : 'orderId' , type : 'input' , label : '订单 ID' , width : 170 } , { key : 'inboundId' , type : 'input' , label : '入库单 ID' , width : 170 } , { key : 'status' , type : 'select' , label : '状态' , options : customsCaseStatusOptions . map ( ( option ) => ( { ... option } ) ) } ]
const columns = [ { key : 'actions' , label : '操作' , fixed : 'left' , width : 175 , slotName : 'actions' , hideable : false , movable : false } , { prop : 'customsNo' , label : '报关编号' , minWidth : 145 } , { prop : 'orderId' , label : '订单 ID' , minWidth : 155 } , { prop : 'inboundId' , label : '入库单 ID' , minWidth : 155 } , { prop : 'checklistCode' , label : '资料清单' , minWidth : 125 } , { prop : 'checklistVersionNo' , label : '版本' , width : 90 } , { prop : 'status' , label : '状态' , width : 110 , slotName : 'status' } , { prop : 'updatedAt' , label : '更新时间' , minWidth : 160 } ]
async function load ( ) { loading . value = true ; error . value = null ; try { const result = await listCustomsCases ( { page : page . value , size : pageSize . value , orderId : String ( filters . orderId || '' ) || undefined , inboundId : String ( filters . inboundId || '' ) || undefined , status : String ( filters . 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 }
@@ -46,12 +47,12 @@ async function selectCustomer(customerId: string) {
function selectOrder ( orderId : string ) { createForm . orderId = orderId ; createForm . inboundId = String ( orders . value . find ( ( item ) => String ( item . orderId ) === orderId ) ? . inboundId || '' ) }
function openCreate ( ) { Object . assign ( createForm , { customerId : '' , orderId : '' , inboundId : '' , containerBookingId : null , checklistVersionId : '' , remark : '' , expectedVersion : 0 } ) ; createError . value = null ; orders . value = [ ] ; createOpen . value = true ; void loadCreateOptions ( ) }
async function create ( ) { if ( ! await formRef . value ? . validate ( ) . catch ( ( ) => false ) ) return ; createSaving . value = true ; createError . value = null ; try { await createCustomsCase ( { ... createForm } ) ; ElMessage . success ( '报关协同已创建' ) ; createOpen . value = false ; await load ( ) } catch ( cause ) { createError . value = getApiErrorDetails ( cause ) } finally { createSaving . value = false } }
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 && access . can ( 'platform:firstleg:customs:submit' ) && ! [ 'SUBMITTED' , 'RELEASED' ] . includes ( detail . value . customsCase . status ) )
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 ( load )
< / 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" row-key="id" preference-key="customs-cases" empty-text="暂无报关协同记录"><template #toolbar-actions><el-button v-if="access.can('platform:firstleg:customs:manage')" type="primary" @click="openCreate"><Plus :size="16" />新增报关</el-button></template><template #actions="{ row }"><el-button v-if="access.can('platform:firstleg:customs:view')" link type="primary" @click="openDetail(row.id)"><Eye :size="15" />查看</el-button></template><template #status="{ row }"><StatusBadge :status="row.status" / > < / template > < / DataTable > < / section > < / ListPageLayout >
< 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" row-key="id" preference-key="customs-cases" empty-text="暂无报关协同记录"><template #toolbar-actions><el-button v-if="access.can('platform:firstleg:customs:manage')" type="primary" @click="openCreate"><Plus :size="16" />新增报关</el-button></template><template #actions="{ row }"><el-button v-if="access.can('platform:firstleg:customs:view')" link type="primary" @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 = "createOpen" title="新增报关协同" width="720px" :confirm-loading="createSaving" @confirm="create"><el-alert v-if="createError" type="error" :closable="false"><ApiErrorMeta :details="createError" /></el-alert><el-alert v-if="optionsError" type="warning" :closable="false" show-icon :title="optionsError"><el-button link type="warning" :loading="optionsLoading || ordersLoading" @click="reloadCreateOptions">重新加载选项</el-button></el-alert><el-form ref="formRef" :model="createForm" :rules="rules" label-position="top"><el-row :gutter="16"><el-col :xs="24" :sm="12"><el-form-item label="客户" prop="customerId"><el-select :model-value="createForm.customerId" filterable placeholder="请选择客户" :loading="optionsLoading" style="width:100%" @update:model-value="selectCustomer(String($event || ''))"><el-option v-for="customer in activeCustomers" :key="String(customer.id)" :label="customer.tenantName" :value="String(customer.id)" /></el-select></el-form-item></el-col><el-col :xs="24" :sm="12"><el-form-item label="订单 / 入库单" prop="orderId"><el-select :model-value="createForm.orderId" filterable placeholder="请先选择客户,再选择已入库订单" :loading="ordersLoading" :disabled="!createForm.customerId" style="width:100%" @update:model-value="selectOrder(String($event || ''))"><el-option v-for="order in orders" :key="String(order.orderId)" :label="`${order.orderNo || order.orderId} / ${order.inboundCode || order.inboundId}`" :value="String(order.orderId)" /></el-select></el-form-item></el-col><el-col :xs="24" :sm="12"><el-form-item label="关联柜次(可选)"><el-select v-model="createForm.containerBookingId" filterable clearable placeholder="请选择柜次" :loading="optionsLoading" style="width:100%"><el-option v-for="container in containers" :key="container.id" :label="`${container.cabinetNo || container.bookingNo}${container.containerNo ? ` / ${container.containerNo}` : ''}`" :value="container.id" /></el-select></el-form-item></el-col><el-col :xs="24" :sm="12"><el-form-item label="资料清单版本 ID" prop="checklistVersionId"><el-input v-model="createForm.checklistVersionId" placeholder="请输入已发布的资料清单版本 ID" /><small>当前服务端尚未提供资料清单版本查询接口。</small></el-form-item></el-col><el-col :xs="24"><el-form-item label="备注"><el-input v-model="createForm.remark" type="textarea" placeholder="请输入报关协同备注(可选)" / > < / el-form-item > < / el-col > < / el-row > < / el-form > < / AppDialog >
< 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="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" > {{ entry.action }} : {{ entry.fromStatus | | ' — ' }} → {{ 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-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 >
< style scoped > . submit - form { margin - top : 18 px } . submit - form + . el - button { margin - bottom : 12 px } h3 { margin : 20 px 0 12 px } small { display : block ; color : var ( -- cc - muted ) } < / style >