Files
client-web/src/modules/first-leg/views/CustomsDocumentsView.vue
T
2026-10-20 14:24:55 +08:00

122 lines
7.1 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
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'
import { MERCHANT_CUSTOMS_STATUS_LABELS, type MerchantCustomsContainer } from '../api/customs'
import { readCustomsDocuments, type CustomsDocumentRow } from '../utils/customsDocumentsReader'
import CustomsDocumentsDialog from '../components/CustomsDocumentsDialog.vue'
import CustomsHelp from '../components/CustomsHelp.vue'
import { useCustomsPendingStore } from '../store/customsPending'
const customsPending = useCustomsPendingStore()
const loading = ref(false)
const errorMessage = ref('')
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: '请输入柜号' },
{ key: 'status', label: '状态', type: 'select', placeholder: '全部', options: Object.entries(MERCHANT_CUSTOMS_STATUS_LABELS).map(([value, label]) => ({ label, value })) },
]
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 },
]
const records = ref<CustomsDocumentRow[]>([])
const dialogOpen = ref(false)
const selectedOrderId = ref('')
const selectedDetail = ref<MerchantCustomsContainer | null>(null)
const filteredRecords = computed(() => {
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'))
&& (!query.status || detail.merchantStatus === query.status),
)
})
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) }]
}
function statusLabel(detail: MerchantCustomsContainer) {
return detail.merchantStatus ? MERCHANT_CUSTOMS_STATUS_LABELS[detail.merchantStatus] ?? '—' : '—'
}
function statusType(detail: MerchantCustomsContainer) {
return detail.merchantStatus === 'CUSTOMS_COMPLETED' || detail.merchantStatus === 'DOCUMENT_UPLOADED'
? 'success' : detail.merchantStatus === 'DECLARING' ? 'primary' : detail.merchantStatus === 'WAITING_UPLOAD' ? 'warning' : 'info'
}
async function load() {
if (loading.value) return
void customsPending.refresh(true)
loading.value = true; errorMessage.value = ''
try {
const { records: loadedRecords, failedCount } = await readCustomsDocuments()
records.value = loadedRecords
if (failedCount) errorMessage.value = `${failedCount} 票订单的报关资料加载失败,请重新加载`
} catch (error) {
records.value = []
errorMessage.value = error instanceof Error ? error.message : '报关资料加载失败,请稍后重试'
} finally { loading.value = false }
}
function openDetail(item: unknown) {
const value = item as CustomsDocumentRow
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">
<QueryToolbar v-model="queryModel" layout="grid" ariaLabel="报关资料查询条件" :fields="queryFields" @search="search" />
<section class="customs-card">
<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 class="customs-list-heading">资料列表<CustomsHelp title="资料统计口径"><p>统计为本客户同柜退税货物汇总,资料覆盖按入仓码计。</p></CustomsHelp></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>
</section>
<CustomsDocumentsDialog v-model="dialogOpen" :order-id="selectedOrderId" :can-edit="selectedDetail?.status !== 'COMPLETED'" :initial-detail="selectedDetail" @updated="updateDetail" />
</div>
</template>
<style scoped>
.customs-page { display: grid; gap: var(--client-section-gap); }
.customs-list-heading { display: flex; align-items: center; gap: 4px; }
.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; }
</style>