2026-09-25 22:23:11 +08:00
|
|
|
<script setup lang="ts">
|
2026-09-29 17:56:03 +08:00
|
|
|
import { computed, onMounted, ref, watch } from 'vue'
|
|
|
|
|
import { RefreshCcw } from '@lucide/vue'
|
|
|
|
|
import DataTable, { type DataTableColumn } from '@/components/common/DataTable.vue'
|
|
|
|
|
import QueryToolbar, { type QueryField, type QueryModel } from '@/components/common/QueryToolbar.vue'
|
|
|
|
|
import type { TableAction } from '@/components/common/tableActions'
|
2026-10-03 03:54:06 +08:00
|
|
|
import { MERCHANT_CUSTOMS_STATUS_LABELS, type MerchantCustomsContainer } from '../api/customs'
|
2026-10-02 12:31:11 +08:00
|
|
|
import { readCustomsDocuments, type CustomsDocumentRow } from '../utils/customsDocumentsReader'
|
2026-09-25 22:23:11 +08:00
|
|
|
import CustomsDocumentsDialog from '../components/CustomsDocumentsDialog.vue'
|
|
|
|
|
|
|
|
|
|
const loading = ref(false)
|
|
|
|
|
const errorMessage = ref('')
|
2026-09-29 17:56:03 +08:00
|
|
|
const queryModel = ref<QueryModel>({ orderNo: '', inboundCode: '', containerNo: '', status: '' })
|
|
|
|
|
const appliedQuery = ref<QueryModel>({ ...queryModel.value })
|
|
|
|
|
const page = ref(1)
|
|
|
|
|
const pageSize = ref(20)
|
|
|
|
|
const queryFields: QueryField[] = [
|
|
|
|
|
{ key: 'orderNo', label: '订单号', type: 'input', placeholder: '请输入订单号' },
|
|
|
|
|
{ key: 'inboundCode', label: '入仓码', type: 'input', placeholder: '请输入入仓码' },
|
|
|
|
|
{ key: 'containerNo', label: '柜号', type: 'input', placeholder: '请输入柜号' },
|
2026-10-03 03:54:06 +08:00
|
|
|
{ key: 'status', label: '状态', type: 'select', placeholder: '全部', options: Object.entries(MERCHANT_CUSTOMS_STATUS_LABELS).map(([value, label]) => ({ label, value })) },
|
2026-09-29 17:56:03 +08:00
|
|
|
]
|
|
|
|
|
const columns: DataTableColumn[] = [
|
|
|
|
|
{ label: '操作', slotName: 'actions', width: 120, fixed: 'left' },
|
|
|
|
|
{ label: '订单号', slotName: 'orderNo', minWidth: 230 },
|
|
|
|
|
{ label: '入仓码', slotName: 'inboundCode', minWidth: 190 },
|
|
|
|
|
{ label: '柜号', slotName: 'containerNo', minWidth: 170 },
|
|
|
|
|
{ label: '实际箱数', prop: 'detail.actualBoxCount', width: 110, align: 'center' },
|
|
|
|
|
{ label: '资料覆盖', slotName: 'coverage', width: 110, align: 'center' },
|
|
|
|
|
{ label: '状态', slotName: 'status', width: 130 },
|
|
|
|
|
]
|
2026-10-02 12:31:11 +08:00
|
|
|
const records = ref<CustomsDocumentRow[]>([])
|
2026-09-25 22:23:11 +08:00
|
|
|
const dialogOpen = ref(false)
|
|
|
|
|
const selectedOrderId = ref('')
|
|
|
|
|
const selectedDetail = ref<MerchantCustomsContainer | null>(null)
|
|
|
|
|
|
|
|
|
|
const filteredRecords = computed(() => {
|
2026-09-29 17:56:03 +08:00
|
|
|
const query = appliedQuery.value
|
|
|
|
|
const matches = (value: string | null | undefined, key: string) => String(value ?? '').toLowerCase().includes(String(query[key] || '').trim().toLowerCase())
|
|
|
|
|
return records.value.filter(({ order, detail }) =>
|
|
|
|
|
matches(order.orderNo, 'orderNo')
|
|
|
|
|
&& matches(order.businessSummary.inboundCode, 'inboundCode')
|
|
|
|
|
&& (matches(detail.containerNo, 'containerNo') || matches(detail.cabinetNo, 'containerNo'))
|
2026-10-03 03:54:06 +08:00
|
|
|
&& (!query.status || detail.merchantStatus === query.status),
|
2026-09-29 17:56:03 +08:00
|
|
|
)
|
2026-09-25 22:23:11 +08:00
|
|
|
})
|
2026-09-29 17:56:03 +08:00
|
|
|
const pagedRecords = computed(() => filteredRecords.value.slice((page.value - 1) * pageSize.value, page.value * pageSize.value))
|
|
|
|
|
const emptyText = computed(() => Object.values(appliedQuery.value).some((value) => String(value || '').trim()) ? '暂无匹配的报关资料,请调整查询条件' : '暂无报关资料')
|
|
|
|
|
|
|
|
|
|
function search() {
|
|
|
|
|
appliedQuery.value = { ...queryModel.value }
|
|
|
|
|
page.value = 1
|
|
|
|
|
}
|
|
|
|
|
watch(pageSize, () => { page.value = 1 })
|
|
|
|
|
watch(() => filteredRecords.value.length, (total) => {
|
|
|
|
|
page.value = Math.min(page.value, Math.max(1, Math.ceil(total / pageSize.value)))
|
|
|
|
|
})
|
|
|
|
|
function rowActions(row: object): TableAction[] {
|
|
|
|
|
return [{ key: 'documents', label: '查看资料', disabled: loading.value, run: () => openDetail(row) }]
|
|
|
|
|
}
|
2026-09-25 22:23:11 +08:00
|
|
|
|
|
|
|
|
function statusLabel(detail: MerchantCustomsContainer) {
|
2026-10-03 03:54:06 +08:00
|
|
|
return detail.merchantStatus ? MERCHANT_CUSTOMS_STATUS_LABELS[detail.merchantStatus] ?? '—' : '—'
|
2026-09-25 22:23:11 +08:00
|
|
|
}
|
|
|
|
|
function statusType(detail: MerchantCustomsContainer) {
|
2026-10-03 03:54:06 +08:00
|
|
|
return detail.merchantStatus === 'CUSTOMS_COMPLETED' || detail.merchantStatus === 'DOCUMENT_UPLOADED'
|
|
|
|
|
? 'success' : detail.merchantStatus === 'DECLARING' ? 'primary' : detail.merchantStatus === 'WAITING_UPLOAD' ? 'warning' : 'info'
|
2026-09-25 22:23:11 +08:00
|
|
|
}
|
|
|
|
|
async function load() {
|
2026-09-29 17:56:03 +08:00
|
|
|
if (loading.value) return
|
2026-09-25 22:23:11 +08:00
|
|
|
loading.value = true; errorMessage.value = ''
|
|
|
|
|
try {
|
2026-10-02 12:31:11 +08:00
|
|
|
const { records: loadedRecords, failedCount } = await readCustomsDocuments()
|
|
|
|
|
records.value = loadedRecords
|
2026-10-02 11:33:51 +08:00
|
|
|
if (failedCount) errorMessage.value = `${failedCount} 票订单的报关资料加载失败,请重新加载`
|
2026-09-25 22:23:11 +08:00
|
|
|
} catch (error) {
|
|
|
|
|
records.value = []
|
|
|
|
|
errorMessage.value = error instanceof Error ? error.message : '报关资料加载失败,请稍后重试'
|
|
|
|
|
} finally { loading.value = false }
|
|
|
|
|
}
|
|
|
|
|
function openDetail(item: unknown) {
|
2026-10-02 12:31:11 +08:00
|
|
|
const value = item as CustomsDocumentRow
|
2026-09-25 22:23:11 +08:00
|
|
|
selectedOrderId.value = value.order.id; selectedDetail.value = value.detail; dialogOpen.value = true
|
|
|
|
|
}
|
|
|
|
|
function updateDetail(value: MerchantCustomsContainer) {
|
|
|
|
|
const found = records.value.find((item) => item.order.id === selectedOrderId.value)
|
|
|
|
|
if (found) found.detail = value
|
|
|
|
|
}
|
|
|
|
|
onMounted(() => { void load() })
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
<div class="customs-page">
|
2026-09-29 17:56:03 +08:00
|
|
|
<QueryToolbar v-model="queryModel" layout="grid" ariaLabel="报关资料查询条件" :fields="queryFields" @search="search" />
|
2026-09-25 22:23:11 +08:00
|
|
|
<section class="customs-card">
|
2026-09-29 17:56:03 +08:00
|
|
|
<DataTable v-model:page="page" v-model:page-size="pageSize" :data="pagedRecords" :columns="columns" :loading="loading" :total="filteredRecords.length" :page-sizes="[10, 20, 50, 100]" :row-actions="rowActions" row-key="order.id" :empty-text="emptyText">
|
|
|
|
|
<template #toolbar><strong>资料列表</strong></template>
|
|
|
|
|
<template #toolbar-actions><el-button :loading="loading" @click="load"><RefreshCcw :size="16" />刷新</el-button></template>
|
|
|
|
|
<template v-if="errorMessage" #error><div class="state-error" role="alert"><span>{{ errorMessage }}</span><el-button :loading="loading" @click="load">重新加载</el-button></div></template>
|
|
|
|
|
<template #orderNo="{ row }">{{ row.order.orderNo || '—' }}</template>
|
|
|
|
|
<template #inboundCode="{ row }">{{ row.order.businessSummary.inboundCode || '—' }}</template>
|
|
|
|
|
<template #containerNo="{ row }">{{ row.detail.containerNo || row.detail.cabinetNo || '—' }}</template>
|
|
|
|
|
<template #coverage="{ row }">{{ row.detail.coveredTaxRefundCount }} / {{ row.detail.taxRefundCount }}</template>
|
|
|
|
|
<template #status="{ row }"><el-tag :type="statusType(row.detail)" effect="light" size="small">{{ statusLabel(row.detail) }}</el-tag></template>
|
|
|
|
|
</DataTable>
|
2026-09-25 22:23:11 +08:00
|
|
|
</section>
|
|
|
|
|
<CustomsDocumentsDialog v-model="dialogOpen" :order-id="selectedOrderId" :can-edit="selectedDetail?.status !== 'COMPLETED'" :initial-detail="selectedDetail" @updated="updateDetail" />
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
2026-09-29 17:56:03 +08:00
|
|
|
.customs-page { display: grid; gap: var(--client-section-gap); }
|
|
|
|
|
.customs-card { overflow: hidden; border: 1px solid var(--client-border); border-radius: var(--client-radius); background: #fff; }
|
|
|
|
|
.state-error { display: flex; min-height: 84px; align-items: center; justify-content: center; gap: 12px; padding: 14px; color: #b42318; }
|
2026-09-25 22:23:11 +08:00
|
|
|
</style>
|